KeepAlive

192 阅读9分钟

前言

我们知道Vue内置了KeepAlive组件,帮助我们在卸载组件的时候对组件进行缓存,让我们在多个组件进行切换的场景下避免了组件销毁,再重新创建带来的性能损耗。示例代码如下:

<template>
    <KeepAlive>
        <component :is="activeComponent" /> 
    </KeepAlive> 
</template>

当动态组件在随着 activeComponent 变化时,如果没有 KeepAlive 做缓存,那么组件在来回切换时就会进行重复的实例化,这里就是通过 KeepAlive 实现了对不活跃组件的缓存。

源码分析

接下来我们来看一下keepAlive的源码

const KeepAliveImpl = { 
    // 组件名称 
    name: `KeepAlive`, 
    // 区别于其他组件的标记 
    __isKeepAlive: true, 
    // 组件的 props 定义 
    props: { 
        include: [String, RegExp, Array],
        exclude: [String, RegExp, Array], 
        max: [String, Number] 
    }, 
    setup(props, {slots}) { 
    // ... 
    // setup 返回一个函数 
    return () => { 
    // ... 
    } 
 }
const isKeepAlive = vnode => vnode.type.__isKeepAlive

可以看到,KeepAlive 组件中,通过 __isKeepAlive 属性来完成对这个内置组件的特殊标记,这样外部可以通过 isKeepAlive 函数来做区分。 然后定义了keepAlive中的几个props

  1. include 表示包含哪些组件可被缓存
  2. exclude 表示排除那些组件
  3. max 表示最大的缓存组件数

KeepAlive 的 render 函数

先来看看 render 函数的源码实现:

 setup(props: KeepAliveProps, { slots }: SetupContext) {
    // 获取组件实例
    const instance = getCurrentInstance()!
    const sharedContext = instance.ctx as KeepAliveContext

    // if the internal renderer is not registered, it indicates that this is server-side rendering,
    // for KeepAlive, we just need to render its children
    if (!sharedContext.renderer) {
      return slots.default
    }

    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')

    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      // 获取组件实例
      const instance = vnode.component!
      // 将缓存的组件挂载到容器中
      // activate 激活函数,核心就是通过 move 方法,将缓存中的 vnode 节点直接挂载到容器中
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // in case props have changed
      // 如果 props 有变动,还是需要对 props 进行 patch
      patch(
        instance.vnode,
        vnode,
        container,
        anchor,
        instance,
        parentSuspense,
        isSVG,
        vnode.slotScopeIds,
        optimized
      )
      // 执行组件的钩子函数
      queuePostRenderEffect(() => {
        instance.isDeactivated = false
        if (instance.a) {
           // 执行 onActivated 钩子
          invokeArrayFns(instance.a)
        }
          // 执行 onVnodeMounted 钩子
        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
      }, parentSuspense)
    }
    // keepAlive组件的deactive方法,卸载态函数 deactivate 
    //核心工作就是将页面中的 DOM 移动到一个隐藏不可见的容器 storageContainer 当中,
    // 这样页面中的元素就被移除了
    sharedContext.deactivate = (vnode: VNode) => {
      // 获取组件实例
      const instance = vnode.component!
      // 将组件移动到隐藏容器中
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      // 执行组件的钩子函数,将用户定义的 onDeactivated 钩子放到状态更新流程后执行
      queuePostRenderEffect(() => {
        // 执行组件的 onDeactivated 钩子
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        // 执行 onVnodeUnmounted
        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
        // 执行完设置组件状态为isDeactivated
        instance.isDeactivated = true
      }, parentSuspense)
    }

    function unmount(vnode: VNode) {
      // reset the shapeFlag so it can be properly unmounted
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense)
    }

    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 || cached.type !== current.type) {
        unmount(cached)
      } else if (current) {
        // current active instance should no longer be kept-alive.
        // we can't unmount it now but it might be later, so reset its flag now.
        resetShapeFlag(current)
      }
      cache.delete(key)
      keys.delete(key)
    }

    // prune cache on include/exclude prop change
    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 }
    )

    // cache sub tree after render
    //定义了一个 pendingCacheKey 变量,用来作为 cache 的缓存 key
    let pendingCacheKey: CacheKey | null = null
    const cacheSubtree = () => {
      // fix #1621, the pendingCacheKey could be 0
      if (pendingCacheKey != null) {
        // 以 pendingCacheKey 作为key 进行缓存收集
        cache.set(pendingCacheKey, getInnerChild(instance.subTree))
      }
    }
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

    onBeforeUnmount(() => {
      cache.forEach(cached => {
        const { subTree, suspense } = instance
        const vnode = getInnerChild(subTree)
        if (cached.type === vnode.type) {
          // current instance will be unmounted as part of keep-alive's unmount
          resetShapeFlag(vnode)
          // but invoke its deactivated hook here
          const da = vnode.component!.da
          da && queuePostRenderEffect(da, suspense)
          return
        }
        unmount(cached)
      })
    })
    // setup 返回一个函数
    return () => {
       // 记录需要被缓存的 key
      pendingCacheKey = null

      if (!slots.default) {
        return null
      }
      // 获取子节点
      const children = slots.default()
      // rawVNode是keepAlive包裹的第一个子节点
      const rawVNode = children[0]
      if (children.length > 1) {
        if (__DEV__) {
          warn(`KeepAlive should contain exactly one component child.`)
        }
         // 子节点数量大于 1 个,不会进行缓存,直接返回
        current = null
        return children
      } else if (
        !isVNode(rawVNode) ||
        (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
          !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
      ) {
        current = null
        return rawVNode
      }
      // suspense 特殊处理,正常节点就是返回节点 vnode
      let vnode = getInnerChild(rawVNode)
      const comp = vnode.type as ConcreteComponent
      // 获取 Component.name 值
      const name = getComponentName(comp)
       // 获取 props 中的属性
      const { include, exclude, max } = props
       // 如果组件 name 不在 include 中或者存在于 exclude 中,则直接返回
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        current = vnode
        return rawVNode
      }
      // 缓存相关,定义缓存 key
      const key = vnode.key == null ? comp : vnode.key
      // 从缓存中取值
      const cachedVNode = cache.get(key)

      // clone vnode if it's reused because we are going to mutate it
      // clone vnode,因为需要重用
      if (vnode.el) {
        vnode = cloneVNode(vnode)
        if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
          rawVNode.ssContent = vnode
        }
      }
      // #1513 it's possible for the returned vnode to be cloned due to attr
      // fallthrough or scopeId, so the vnode here may not be the final vnode
      // that is mounted. Instead of caching it directly, we store the pending
      // key and cache `instance.subTree` (the normalized vnode) in
      // beforeMount/beforeUpdate hooks.
       // 给 pendingCacheKey 赋值,将在 beforeMount/beforeUpdate 中被使用
      pendingCacheKey = key
      // 如果存在缓存的 vnode 元素
      if (cachedVNode) {
        // 复制挂载状态
        // 复制 DOM
        vnode.el = cachedVNode.el
        // 复制 component
        vnode.component = cachedVNode.component
        if (vnode.transition) {
          // recursively update transition hooks on subTree
          setTransitionHooks(vnode, vnode.transition!)
        }
        // avoid vnode being mounted as fresh
        // 增加 shapeFlag 类型 COMPONENT_KEPT_ALIVE
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        // make this key the freshest
        // 把缓存的 key 移动到到队首
        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)
        }
      }
      // avoid vnode being unmounted
       // 增加 shapeFlag 类型 COMPONENT_SHOULD_KEEP_ALIVE,避免被卸载
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      current = vnode
      // 返回 vnode 节点
      return rawVNode
    }
  }

可以看到返回的这个 render 函数执行的结果就是返回被 KeepAlive 包裹的子节点的 vnode 只不过在返回子节点的过程中做了很多处理而已,如果子节点数量大于一个,那么将不会被 keepAlive,直接返回子节点的 vnode,如果组件 name 不在用户定义的 include 中或者存在于 exclude 中,也会直接返回子节点的 vnode

缓存设计

接着来看后续的缓存步骤

    // cache sub tree after render
    //定义了一个 pendingCacheKey 变量,用来作为 cache 的缓存 key
    let pendingCacheKey: CacheKey | null = null
    const cacheSubtree = () => {
      // fix #1621, the pendingCacheKey could be 0
      if (pendingCacheKey != null) {
        // 以pendingCacheKey作为key 进行缓存收集
        cache.set(pendingCacheKey, getInnerChild(instance.subTree))
      }
    }
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

首先定义了一个 pendingCacheKey 变量,用来作为 cache 的缓存 key。在初始化 KeepAlive 组件的时候,此时还没有缓存,那么只会将 key 添加到 keys 这样一个 Set 中,在组件的 onMounted 和 onUpdated 钩子中进行缓存组件的 vnode 收集,因为这个时候收集到的 vnode 节点是稳定不会变的。

另外,注意到 props 中还有一个 max 变量用来标记最大的缓存数量,这个缓存策略就是类似于LRU的方式实现的。在缓存重新被激活时,之前缓存的 key 会被重新添加到Set的前面,标记为最近的一次缓存,如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。

最后,当缓存的节点被重新激活时,则会将缓存中的节点的 el 属性赋值给新的 vnode 节点,从而减少了再进行 patch 生成 DOM 的过程,这里也说明了 KeepAlive 核心目的就是缓存 DOM 元素。

激活态设计

上述源码中,当组件被添加到 KeepAlive 缓存池中时,也会为 vnode 节点的 shapeFlag 添加两额外的两个属性,分别是 COMPONENT_KEPT_ALIVE 和 COMPONENT_SHOULD_KEEP_ALIVE。我们先说 COMPONENT_KEPT_ALIVE 这个属性,当一个节点被标记为 COMPONENT_KEPT_ALIVE 时,会在 processComponent 时进行特殊处理:

const processComponent = (...) => { 
    if (n1 == null) { 
    // 处理 KeepAlive 组件 
        if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { 
        // 执行 activate 钩子 
        ;(parentComponent!.ctx as KeepAliveContext).activate(
        n2, container, anchor, isSVG, optimized ) 
        } else { 
        mountComponent( n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized 
        ) 
       } 
    } 
    else { // 更新组件 } 
 }

可以看到,在 processComponent 阶段如果是 keepAlive 的组件,在挂载过程中,不会执行执行 mountComponent 的逻辑,因为已经缓存好了,所以只需要再次调用 activate 激活就好了。接下来看看这个激活函数做了哪些事

setup(props: KeepAliveProps, { slots }: SetupContext) {

 sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
   // 获取组件实例
   const instance = vnode.component!
   // 将缓存的组件挂载到容器中
   // activate 激活函数,核心就是通过 move 方法,将缓存中的 vnode 节点直接挂载到容器中
   move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
   // in case props have changed
   // 如果 props 有变动,还是需要对 props 进行 patch
   patch(
     instance.vnode,
     vnode,
     container,
     anchor,
     instance,
     parentSuspense,
     isSVG,
     vnode.slotScopeIds,
     optimized
   )
   // 执行组件的钩子函数
   queuePostRenderEffect(() => {
     instance.isDeactivated = false
     if (instance.a) {
        // 执行 onActivated 钩子
       invokeArrayFns(instance.a)
     }
       // 执行 onVnodeMounted 钩子
     const vnodeHook = vnode.props && vnode.props.onVnodeMounted
     if (vnodeHook) {
       invokeVNodeHook(vnodeHook, instance.parent, vnode)
     }
   }, parentSuspense)
 }
}

可以直观的看到 activate 激活函数,核心就是通过 move 方法,将缓存中的 vnode 节点直接挂载到容器中,同时为了防止 props 变化导致组件变化,也会执行 patch 方法来更新组件。当这一切都执行完成后,最后再通过 queuePostRenderEffect 函数,将用户定义的 onActivated 钩子放到状态更新流程后执行。

卸载态设计

接下来我们再看另一个标记态:COMPONENT_SHOULD_KEEP_ALIVE,我们看一下组件的卸载函数 unmount 的设计:

const unmount = (vnode, parentComponent, parentSuspense, doRemove = false) => { 
    // ... 
    const { shapeFlag } = vnode 
    if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { 
        ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode) 
        return 
        } 
        // ... 
   }

可以看到,如果 shapeFlag 上存在 COMPONENT_SHOULD_KEEP_ALIVE 属性的话,那么将会执行 ctx.deactivate 方法,我们再来看一下 deactivate 函数的定义:

 setup(props: KeepAliveProps, { slots }: SetupContext) {

    // 创建一个隐藏容器
    const storageContainer = createElement('div')

 
    // keepAlive组件的deactive方法,卸载态函数 deactivate 核心工作就是将页面中的 DOM 移动到一个隐藏不可见的容器 storageContainer 当中,这样页面中的元素就被移除了
    sharedContext.deactivate = (vnode: VNode) => {
      // 获取组件实例
      const instance = vnode.component!
      // 将组件移动到隐藏容器中
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      // 执行组件的钩子函数,将用户定义的 onDeactivated 钩子放到状态更新流程后执行
      queuePostRenderEffect(() => {
        // 执行组件的 onDeactivated 钩子
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        // 执行 onVnodeUnmounted
        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
        // 执行完设置组件状态为isDeactivated
        instance.isDeactivated = true
      }, parentSuspense)
    }
  }

卸载态函数 deactivate 核心工作就是将页面中的 DOM 移动到一个隐藏不可见的容器 storageContainer 当中,这样页面中的元素就被移除了。当这一切都执行完成后,最后再通过 queuePostRenderEffect 函数,将用户定义的 onDeactivated 钩子放到状态更新流程后执行。

总结

  1. 组件是通过类似于 LRU 的缓存机制来缓存的,并为缓存的组件 vnode 的 shapeFlag 属性打上 COMPONENT_KEPT_ALIVE 属性,当组件在 processComponent 挂载时,如果存在 COMPONENT_KEPT_ALIVE 属性,则会执行激活函数,激活函数内执行具体的缓存节点挂载逻辑。
  2. 缓存不是越多越好,因为所有的缓存节点都会被存在 cache 中,如果过多,则会增加内存负担。
  3. 处理缓存的方式就是在缓存重新被激活时,之前缓存的 key 会被重新添加到Set集合的头部,标记为最近的一次缓存,如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被丢弃。