likes
comments
collection
share

Vue3源码系列 (六) KeepAlive

作者站长头像
站长
· 阅读数 39

KeepAlive是个抽象组件,自身不会渲染一个 DOM 元素,也不会出现在父组件链中,我们用它来缓存组件的状态。KeepAlive只对插入的单个组件起效果,因此一般只给它安排一个组件。适合与componentrouter-view搭配使用。

一、ts 类型

先来和KeepAlive相关的类型:

  • MatchPattern:匹配模式,是传递的参数includeexclude接收的类型;
  • KeepAliveProps:可传递三个参数,include指定被缓存的组件,exclude指定不缓存的组件,max指定最大缓存组件数量;
  • Cache:变量cache的类型,cache用于缓存组件;
  • Keys:变量keys的类型,keys用于存储被缓存组件对应的key,用于LRU算法;
  • KeepAliveContext:继承自ComponentRenderContext,并拓展了rendereractivatedeactivate三个字段。
type MatchPattern = string | RegExp | (string | RegExp)[]
​
export interface KeepAliveProps {
  include?: MatchPattern
  exclude?: MatchPattern
  max?: number | string
}
​
type CacheKey = string | number | symbol | ConcreteComponent
type Cache = Map<CacheKey, VNode>
type Keys = Set<CacheKey>
​
export interface KeepAliveContext extends ComponentRenderContext {
  renderer: RendererInternals
  activate: (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    isSVG: boolean,
    optimized: boolean
  ) => void
  deactivate: (vnode: VNode) => void
}

二、KeepAliveImpl

1. KeepAliveImpl 的成员

KeepAliveImplKeepAlive的核心实现。包含name__isKeepAlive(用于判断组件是否是KeepAlive),props(上面提到的KeepAliveProps类型)以及setup方法。KeepAlive与实例化的renderer通过上下文来传递信息。在当前实例的上下文对象ctx上暴露了activatedeactivate两个方法。

const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
​
  // Marker for special handling inside the renderer. We are not using a ===
  // check directly on KeepAlive in the renderer, because importing it directly
  // would prevent it from being tree-shaken.
  __isKeepAlive: true,
​
  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },
​
  setup(props: KeepAliveProps, { slots }: SetupContext) {
    // ...
  }
}

2. setup

setup中,拿到当前实例的上下文对象,并挂上activatedeactivate两个方法。

activate中,通过调用patch来进行对比更新,以同步props传参可能的变更;调整组件为激活状态instance.isDeactivated = false;调用实例的onActived钩子等。

{
  // ...
  setup(props: KeepAliveProps, { slots }: SetupContext) {
    const instance = getCurrentInstance()!
    // KeepAlive communicates with the instantiated renderer via the
    // ctx where the renderer passes in its internals,
    // and the KeepAlive instance exposes activate/deactivate implementations.
    // The whole point of this is to avoid importing KeepAlive directly in the
    // renderer to facilitate tree-shaking.
    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 (__SSR__ && !sharedContext.renderer) {
      return () => {
        const children = slots.default && slots.default()
        return children && children.length === 1 ? children[0] : children
      }
    }
​
    // 用于缓存组件
    const cache: Cache = new Map()
    const keys: Keys = new Set()
    let current: VNode | null = null
​
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      ;(instance as any).__v_cache = cache
    }
​
    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!
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // in case props have changed
      patch(
        instance.vnode,
        vnode,
        container,
        anchor,
        instance,
        parentSuspense,
        isSVG,
        vnode.slotScopeIds,
        optimized
      )
      queuePostRenderEffect(() => {
        instance.isDeactivated = false
        if (instance.a) {
          invokeArrayFns(instance.a)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
      }, parentSuspense)
​
      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        // Update components tree
        devtoolsComponentAdded(instance)
      }
    }
    
    // ...
  }
}

deactivate中的操作类似。

{
  setup(){
    // ...
    
    sharedContext.deactivate = (vnode: VNode) => {
      const instance = vnode.component!
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      queuePostRenderEffect(() => {
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
        instance.isDeactivated = true
      }, parentSuspense)
​
      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        // Update components tree
        devtoolsComponentAdded(instance)
      }
    }
    
    // ...
  }
}

随之声明了组件卸载以及销毁缓存的方法。基本都用在setup返回的函数里。

{
  setup(){
    // ...
    
    // 组件卸载
    function unmount(vnode: VNode) {
      // reset the shapeFlag so it can be properly unmounted
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense, true)
    }
​
    // 根据组件名 和 filter 销毁缓存
    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)
    }
    
    // ...
  }
}

使用watch API侦听includeexclude的变化,一旦改变,根据match函数得到的filter去销毁相应的缓存。match函数根据includeexclude匹配模式来筛选出需要被销毁的缓存。

{
  setup( props ){
    // ...
    
     // 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 }
    )
    
    // ...
  }
}
​
// match
function matches(pattern: MatchPattern, name: string): boolean {
  if (isArray(pattern)) {
    return pattern.some((p: string | RegExp) => matches(p, name))
  } else if (isString(pattern)) {
    return pattern.split(',').includes(name)
  } else if (pattern.test) {
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

接下来给onMountedonUpdatedonBeforeUnmount安排任务。在挂载和更新时执行cacheSubtree来缓存子组件树,卸载前调用其中的组件的onDeactived钩子,再卸载组件。

{
  setup(){
    // ...
    
    // cache sub tree after render
    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)
    
    // 卸载前,在其中调用组件的 onDeactived 钩子
    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)
      })
    })
    
    return () => {
      // ...
    }
  }
}

最后是KeepAlivesetup的返回值的部分了,这里setup返回一个函数。可以看到KeepAlive只对插入单个组件有效果,即rawVNode = $slots.default()[0]。根据rawVNode获取到vnodelet vnode = getInnerChild(rawVNode)

以下各项条件会直接返回该组件,且无法进入缓存流程。

  • 默认插槽有多个组件,即slots.default()的长度大于1,则直接返回$slots.default()
  • rawVNode不属于VNode类型,直接返回rawVNode
  • rawVNode的形状标志被重置了,发生在当前组件是缓存组件且处于卸载流程时;

此外,当rawVNode是异步组件时,也会返回rawVNode,但是缓存程序会执行。

而当rawVNode未被直接返回,且不是异步组件时:

  • 如果已有缓存,则取缓存的值更新到vnode里,更新key的位置(LRU算法),最后返回vnode
  • 没有缓存的值,则进行缓存,并返回vnode
() => {
  pendingCacheKey = null
​
  if (!slots.default) {
    return null
  }
​
  // 取默认插槽中的第一个组件
  const children = slots.default()
  const rawVNode = children[0]
  
  // 如果默认插槽中有多个组件,则直接返回它们,导致无法进入缓存流程
  if (children.length > 1) {
    if (__DEV__) {
      warn(`KeepAlive should contain exactly one component child.`)
    }
    current = null
    
    // 返回这些组件
    return children
  } else if (
    // 不是vnode,或者没有缓存标志了,直接返回,不进入缓存流程
    !isVNode(rawVNode) ||
    (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
      !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
  ) {
    current = null
    return rawVNode
  }
​
  let vnode = getInnerChild(rawVNode)
  /** 把 getInnerChild 函数搬到这里方便阅读
    *
    *   function getInnerChild(vnode: VNode) {
    *     return vnode.shapeFlag & ShapeFlags.SUSPENSE ? vnode.ssContent! : vnode
    *   }
    */
  const comp = vnode.type as ConcreteComponent
​
  // for async components, name check should be based in its loaded
  // inner component if available
  const name = getComponentName(
    isAsyncWrapper(vnode)
      ? (vnode.type as ComponentOptions).__asyncResolved || {}
      : comp
  )
​
  const { include, exclude, max } = props
​
  // 根据 匹配模式 和 组件名 校验
  if (
    (include && (!name || !matches(include, name))) ||
    (exclude && name && matches(exclude, name))
  ) {
    current = vnode
    return rawVNode
  }
​
  // 取缓存的值
  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
  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 = key
​
  // 存在缓存的值,就
  if (cachedVNode) {
    // copy over mounted state
    vnode.el = cachedVNode.el
    vnode.component = cachedVNode.component
    if (vnode.transition) {
      // recursively update transition hooks on subTree
      setTransitionHooks(vnode, vnode.transition!)
    }
    // avoid vnode being mounted as fresh
    vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
    // make this key the freshest
    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
  vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
​
  current = vnode
  return isSuspense(rawVNode.type) ? rawVNode : vnode
}

三、KeepAlive

KeepAlive就是KeepAliveImpl,重新声明了类型。

/ export the public type for h/tsx inference
// also to avoid inline import() in generated d.ts files
export const KeepAlive = KeepAliveImpl as any as {
  __isKeepAlive: true
  new (): {
    $props: VNodeProps & KeepAliveProps
  }
}

四、onActivedonDeactived

这两个生命周期钩子通过registerKeepAliveHook来注册。

export function onActivated(
  hook: Function,
  target?: ComponentInternalInstance | null
) {
  registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
}
​
export function onDeactivated(
  hook: Function,
  target?: ComponentInternalInstance | null
) {
  registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
}

registerKeepAliveHookhook包装成wrappedHook并注入钩子。此外,通过injectToKeepAliveRoot把包装的钩子wrappedHook注入到KeepAlive里相应的钩子列表的前面(unshift方法),之后可以不用再去递归遍历整个组件树了查找相应组件的onActivedonDeactived钩子了,只需要遍历调用KeepAlive中的钩子列表,当然,需要注意在组件卸载时移除相应的钩子。

function registerKeepAliveHook(
  hook: Function & { __wdc?: Function },
  type: LifecycleHooks,
  target: ComponentInternalInstance | null = currentInstance
) {
  // cache the deactivate branch check wrapper for injected hooks so the same
  // hook can be properly deduped by the scheduler. "__wdc" stands for "with
  // deactivation check".
  const wrappedHook =
    hook.__wdc ||
    (hook.__wdc = () => {
      // only fire the hook if the target instance is NOT in a deactivated branch.
      let current: ComponentInternalInstance | null = target
      while (current) {
        if (current.isDeactivated) {
          return
        }
        current = current.parent
      }
      return hook()
    })
  injectHook(type, wrappedHook, target)
  // In addition to registering it on the target instance, we walk up the parent
  // chain and register it on all ancestor instances that are keep-alive roots.
  // This avoids the need to walk the entire component tree when invoking these
  // hooks, and more importantly, avoids the need to track child components in
  // arrays.
  if (target) {
    let current = target.parent
    while (current && current.parent) {
      if (isKeepAlive(current.parent.vnode)) {
        injectToKeepAliveRoot(wrappedHook, type, target, current)
      }
      current = current.parent
    }
  }
}
​
// injectHook(type, hook, keepAliveRoot, true /* prepend */)
// true 表示把 hook 放到 keepAliveRoot[type] 对应的钩子列表的前面,即使用 unshift() 方法
function injectToKeepAliveRoot(
  hook: Function & { __weh?: Function },
  type: LifecycleHooks,
  target: ComponentInternalInstance,
  keepAliveRoot: ComponentInternalInstance
) {
  // injectHook wraps the original for error handling, so make sure to remove
  // the wrapped version.
  const injected = injectHook(type, hook, keepAliveRoot, true /* prepend */)
  // 卸载时移除
  onUnmounted(() => {
    remove(keepAliveRoot[type]!, injected)
  }, target)
}
转载自:https://juejin.cn/post/7157241400042455047
评论
请登录