Skip to content

37.keep-alive 平时在哪里使用?

1.1 概念

keep-alive 是 vue 中的内置组件,能在组件切换过程会缓存组件的实例,而不是销毁它们。在组件再次重新激活时可以通过缓存的实例拿到之前渲染的 DOM 进行渲染,无需重新生成节点。

1.2 使用场景

动态组件可以采用keep-alive进行缓存

vue
<template>
  <keep-alive :include="whiteList" :exclude="blackList" :max="count">
    <component :is="component"></component>
  </keep-alive>
</template>
<template>
  <keep-alive :include="whiteList" :exclude="blackList" :max="count">
    <component :is="component"></component>
  </keep-alive>
</template>

在路由中使用 keep-alive

vue
<template>
  <!-- vue2写法 -->
  <keep-alive :include="whiteList" :exclude="blackList" :max="count">
    <router-view></router-view>
  </keep-alive>

  <!-- vue3写法 -->
  <router-view v-slot="{ Component }">
    <keep-alive :include="whiteList" :exclude="blackList" :max="count">
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>
<template>
  <!-- vue2写法 -->
  <keep-alive :include="whiteList" :exclude="blackList" :max="count">
    <router-view></router-view>
  </keep-alive>

  <!-- vue3写法 -->
  <router-view v-slot="{ Component }">
    <keep-alive :include="whiteList" :exclude="blackList" :max="count">
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>

也可以通过 meta 属性指定哪些页面需要缓存,哪些不需要

vue
<template>
  <!-- vue2写法 -->
  <keep-alive>
    <!-- 需要缓存的视图组件 -->
    <router-view v-if="$route.meta.keepAlive"></router-view>
  </keep-alive>
  <!-- 不需要缓存的视图组件 -->
  <router-view v-if="!$route.meta.keepAlive"></router-view>

  <!-- vue3写法 -->
  <router-view v-slot="{ Component }">
    <transition>
      <keep-alive>
        <!-- 需要缓存的视图组件 -->
        <component :is="Component" v-if="route.meta.keepAlive" />
      </keep-alive>
      <!-- 不需要缓存的视图组件 -->
      <component :is="Component" v-if="!route.meta.keepalive" />
    </transition>
  </router-view>
</template>
<template>
  <!-- vue2写法 -->
  <keep-alive>
    <!-- 需要缓存的视图组件 -->
    <router-view v-if="$route.meta.keepAlive"></router-view>
  </keep-alive>
  <!-- 不需要缓存的视图组件 -->
  <router-view v-if="!$route.meta.keepAlive"></router-view>

  <!-- vue3写法 -->
  <router-view v-slot="{ Component }">
    <transition>
      <keep-alive>
        <!-- 需要缓存的视图组件 -->
        <component :is="Component" v-if="route.meta.keepAlive" />
      </keep-alive>
      <!-- 不需要缓存的视图组件 -->
      <component :is="Component" v-if="!route.meta.keepalive" />
    </transition>
  </router-view>
</template>

1.3 原理

1).vue2 原理

js
export default {
  name: 'keep-alive',
  abstract: true, // 不会放到对应的lifecycle
  props: {
    include: patternTypes, // 白名单
    exclude: patternTypes, // 黑名单
    max: [String, Number] // 缓存的最大个数
  },

  created () {
    this.cache = Object.create(null) // 缓存列表
    this.keys = []  // 缓存的key列表
  },

  destroyed () {
    for (const key in this.cache) { // keep-alive销毁时 删除所有缓存
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () { // 监控缓存列表
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot) 、// 获得第一个组件
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if ( // 获取组件名 看是否需要缓存,不需要缓存则直接返回
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }
      const { cache, keys } = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key // 生成缓存的key
      if (cache[key]) { // 如果有key 将组件实例直接复用
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key) // lru算法
      } else {
        cache[key] = vnode // 缓存组件
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode) // 超过最大限制删除第一个
        }
      }

      vnode.data.keepAlive = true // 在firstComponent的vnode中增加keep-alive属性
    }
    return vnode || (slot && slot[0])
  }
}
export default {
  name: 'keep-alive',
  abstract: true, // 不会放到对应的lifecycle
  props: {
    include: patternTypes, // 白名单
    exclude: patternTypes, // 黑名单
    max: [String, Number] // 缓存的最大个数
  },

  created () {
    this.cache = Object.create(null) // 缓存列表
    this.keys = []  // 缓存的key列表
  },

  destroyed () {
    for (const key in this.cache) { // keep-alive销毁时 删除所有缓存
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () { // 监控缓存列表
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot) 、// 获得第一个组件
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if ( // 获取组件名 看是否需要缓存,不需要缓存则直接返回
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }
      const { cache, keys } = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key // 生成缓存的key
      if (cache[key]) { // 如果有key 将组件实例直接复用
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key) // lru算法
      } else {
        cache[key] = vnode // 缓存组件
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode) // 超过最大限制删除第一个
        }
      }

      vnode.data.keepAlive = true // 在firstComponent的vnode中增加keep-alive属性
    }
    return vnode || (slot && slot[0])
  }
}

2).vue3 原理

js
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  __isKeepAlive: true,

  props: {
    include: [String, RegExp, Array], // 包含需要缓存的组件名称规则
    exclude: [String, RegExp, Array], // 排除不需要缓存的组件名称规则
    max: [String, Number]  // 最大缓存的组件实例数量
  },

  setup(props: KeepAliveProps, { slots }: SetupContext) {
    const instance = getCurrentInstance()!

    const sharedContext = instance.ctx as KeepAliveContext

    // 缓存组件实例的 Map
    const cache: Cache = new Map()
    // 缓存组件实例的键集合
    const keys: Keys = new Set()
    let current: VNode | null = null

    const parentSuspense = instance.suspense

    const {
      renderer: {
        p: patch,
        m: move,
        um: _unmount,
        o: { createElement }
      }
    } = sharedContext
    const storageContainer = createElement('div') // 用于存储组件对应DOM的容器
    // 激活组件时调用的方法
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      const instance = vnode.component!
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // ...
    }
    // 失活组件时调用的方法
    sharedContext.deactivate = (vnode: VNode) => {
      const instance = vnode.component!
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      // ...
    }
    // 卸载组件
    function unmount(vnode: VNode) {
      // reset the shapeFlag so it can be properly unmounted
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense, true)
    }
    // 处理缓存
    function pruneCache(filter?: (name: string) => boolean) {
      cache.forEach((vnode, key) => {
        const name = getComponentName(vnode.type as ConcreteComponent)
        if (name && (!filter || !filter(name))) {
          pruneCacheEntry(key)
        }
      })
    }
    // 移除头部缓存
    function pruneCacheEntry(key: CacheKey) {
      const cached = cache.get(key) as VNode
      if (!current || !isSameVNodeType(cached, current)) {
        unmount(cached)
      } else if (current) {
        resetShapeFlag(current)
      }
      cache.delete(key)
      keys.delete(key)
    }

    // 监听 include 和 exclude 属性的变化,然后清理缓存
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        include && pruneCache(name => matches(include, name))
        exclude && pruneCache(name => !matches(exclude, name))
      },
      // prune post-render after `current` has been updated
      { flush: 'post', deep: true }
    )

    // 在渲染后缓存子树
    let pendingCacheKey: CacheKey | null = null
    const cacheSubtree = () => {
      // fix #1621, the pendingCacheKey could be 0
      if (pendingCacheKey != null) {
        cache.set(pendingCacheKey, getInnerChild(instance.subTree))
      }
    }
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

    // ...

    return () => {
      // ...
      if (cachedVNode) {
        vnode.el = cachedVNode.el // 复用缓存dom
        vnode.component = cachedVNode.component
        if (vnode.transition) {
          // recursively update transition hooks on subTree
          setTransitionHooks(vnode, vnode.transition!)
        }
        // 避免 vnode 作为新的组件实例被挂载
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        // LRU算法
        keys.delete(key)
        keys.add(key)
      } else {
        keys.add(key)
        // prune oldest entry
        if (max && keys.size > parseInt(max as string, 10)) {
          pruneCacheEntry(keys.values().next().value) // 删除缓存中的第一个
        }
      }
      // 避免组件卸载
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      current = vnode
      return isSuspense(rawVNode.type) ? rawVNode : vnode
    }
  }
}
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  __isKeepAlive: true,

  props: {
    include: [String, RegExp, Array], // 包含需要缓存的组件名称规则
    exclude: [String, RegExp, Array], // 排除不需要缓存的组件名称规则
    max: [String, Number]  // 最大缓存的组件实例数量
  },

  setup(props: KeepAliveProps, { slots }: SetupContext) {
    const instance = getCurrentInstance()!

    const sharedContext = instance.ctx as KeepAliveContext

    // 缓存组件实例的 Map
    const cache: Cache = new Map()
    // 缓存组件实例的键集合
    const keys: Keys = new Set()
    let current: VNode | null = null

    const parentSuspense = instance.suspense

    const {
      renderer: {
        p: patch,
        m: move,
        um: _unmount,
        o: { createElement }
      }
    } = sharedContext
    const storageContainer = createElement('div') // 用于存储组件对应DOM的容器
    // 激活组件时调用的方法
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      const instance = vnode.component!
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // ...
    }
    // 失活组件时调用的方法
    sharedContext.deactivate = (vnode: VNode) => {
      const instance = vnode.component!
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      // ...
    }
    // 卸载组件
    function unmount(vnode: VNode) {
      // reset the shapeFlag so it can be properly unmounted
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense, true)
    }
    // 处理缓存
    function pruneCache(filter?: (name: string) => boolean) {
      cache.forEach((vnode, key) => {
        const name = getComponentName(vnode.type as ConcreteComponent)
        if (name && (!filter || !filter(name))) {
          pruneCacheEntry(key)
        }
      })
    }
    // 移除头部缓存
    function pruneCacheEntry(key: CacheKey) {
      const cached = cache.get(key) as VNode
      if (!current || !isSameVNodeType(cached, current)) {
        unmount(cached)
      } else if (current) {
        resetShapeFlag(current)
      }
      cache.delete(key)
      keys.delete(key)
    }

    // 监听 include 和 exclude 属性的变化,然后清理缓存
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        include && pruneCache(name => matches(include, name))
        exclude && pruneCache(name => !matches(exclude, name))
      },
      // prune post-render after `current` has been updated
      { flush: 'post', deep: true }
    )

    // 在渲染后缓存子树
    let pendingCacheKey: CacheKey | null = null
    const cacheSubtree = () => {
      // fix #1621, the pendingCacheKey could be 0
      if (pendingCacheKey != null) {
        cache.set(pendingCacheKey, getInnerChild(instance.subTree))
      }
    }
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

    // ...

    return () => {
      // ...
      if (cachedVNode) {
        vnode.el = cachedVNode.el // 复用缓存dom
        vnode.component = cachedVNode.component
        if (vnode.transition) {
          // recursively update transition hooks on subTree
          setTransitionHooks(vnode, vnode.transition!)
        }
        // 避免 vnode 作为新的组件实例被挂载
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        // LRU算法
        keys.delete(key)
        keys.add(key)
      } else {
        keys.add(key)
        // prune oldest entry
        if (max && keys.size > parseInt(max as string, 10)) {
          pruneCacheEntry(keys.values().next().value) // 删除缓存中的第一个
        }
      }
      // 避免组件卸载
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      current = vnode
      return isSuspense(rawVNode.type) ? rawVNode : vnode
    }
  }
}

核心原理就是缓存 + LRU 算法

1.4 keep-alive 中数据更新问题

beforeRouteEnter:在有 vue-router 的项目,每次进入路由的时候,都会执行beforeRouteEnter

js
beforeRouteEnter(to, from, next){
  next(vm=>{
    vm.getData()  // 获取数据
  })
},
beforeRouteEnter(to, from, next){
  next(vm=>{
    vm.getData()  // 获取数据
  })
},

actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子

js
activated(){
	this.getData() // 获取数据
},
activated(){
	this.getData() // 获取数据
},

Released under the MIT License.