Vue3读源码系列(十一):keepalive全局组件实现原理

52 阅读6分钟

keepalive帮助我们保存组件的状态,它的原理并不复杂,就是保存默认插槽传入的组件在卸载时的状态,包括保存子组件的组件实例和el,我们直接看源码:

packages/runtime-core/src/components/KeepAlive.ts
// KeepAlive组件
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) {
    // 获取当前实例
    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.
    
    // instance.ctx赋值给sharedContext
    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
      }
    }
    // 声明一个用于缓存的map key是子组件对象 值是subTree(子组件render执行后的vnode)
    const cache: Cache = new Map()
    // keys用来存储cache的key
    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
    // 创建一个div容器 用于存放子组件的el
    const storageContainer = createElement('div')
    // 挂载activate方法 在patch子组件的过程中判断shapeFlag为COMPONENT_KEPT_ALIVE时会执行该方法
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      // 组件实例
      const instance = vnode.component!
      // move的作用是将vnode.el移动到container中 以anchor为锚点执行insertBefore
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // in case props have changed
      // patch防止props发生改变
      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方法 组件卸载时判断shapeFlag为COMPONENT_SHOULD_KEEP_ALIVE时调用
    sharedContext.deactivate = (vnode: VNode) => {
      const instance = vnode.component!
      // 将vnode.el挂载到storageContainer(起到缓存el的作用)
      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)
      }
    }

    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)
        }
      })
    }
    // 根据key移除对应的缓存
    function pruneCacheEntry(key: CacheKey) {
      const cached = cache.get(key) as VNode
      if (!current || !isSameVNodeType(cached, current)) {
        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
    // 深度监听include和exclude 当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 }
    )

    // cache sub tree after render
    let pendingCacheKey: CacheKey | null = null
    // 缓存subTree
    const cacheSubtree = () => {
      // fix #1621, the pendingCacheKey could be 0
      if (pendingCacheKey != null) {
        cache.set(pendingCacheKey, getInnerChild(instance.subTree))
      }
    }
    // 生命周期钩子 执行cacheSubtree
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)
    // keepalive 组件卸载前清除所有缓存
    onBeforeUnmount(() => {
      cache.forEach(cached => {
        const { subTree, suspense } = instance
        const vnode = getInnerChild(subTree)
        if (cached.type === vnode.type && cached.key === vnode.key) {
          // 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)
      })
    })
    // keepalive render函数
    return () => {
      pendingCacheKey = null

      if (!slots.default) {
        return null
      }
      // 执行默认插槽函数 获取插槽内容vnode数组
      const children = slots.default()
      // 子组件vnode
      const rawVNode = children[0]
      // keep-alive只能有一个子节点
      // 如果有多个子节点或者子节点不是有状态组件且不是suspense组件 则直接返回子节点
      if (children.length > 1) {
        if (__DEV__) {
          warn(`KeepAlive should contain exactly one component child.`)
        }
        current = null
        return children
      } else if (
        !isVNode(rawVNode) ||
        (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
          !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
      ) {
        current = null
        return rawVNode
      }
      // 如果是suspense组件 返回ssContent
      let vnode = getInnerChild(rawVNode)
      // 获取组件对象
      const comp = vnode.type as ConcreteComponent

      // for async components, name check should be based in its loaded
      // inner component if available
      // 获取组件name
      const name = getComponentName(
        isAsyncWrapper(vnode)
          ? (vnode.type as ComponentOptions).__asyncResolved || {}
          : comp
      )

      const { include, exclude, max } = props
      // 如果存在include且include中没有匹配到name 或者 
      // 存在exclude且exclude中匹配到了name 直接返回子节点 不做缓存
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        current = vnode
        return rawVNode
      }
      // 组件使用组件对象作为key
      const key = vnode.key == null ? comp : vnode.key
      // 从cache中获取缓存的vnode
      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.
      // 等待被缓存的key
      pendingCacheKey = key
      if (cachedVNode) {
        // 如果已经被缓存 将缓存vnode的el和组件实例赋值给当前vnode
        // 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标记为COMPONENT_KEPT_ALIVE 
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        // make this key the freshest
        // 这里删除再添加是因为set数据类型会记录key的插入顺序
        // 这里将key的插入顺序更新为最新 以便后续删除最久没有被访问的组件
        keys.delete(key)
        keys.add(key)
      } else {
        // 没有被缓存
        // 添加到keys
        keys.add(key)
        // prune oldest entry
        // 如果keys的长度大于max 则删除最久没有被访问的组件
        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组件挂载,首先执行其setup函数,里面声明了用于存储子组件vnode的cache和用于存储子组件对象的keys(这个keys是实现LUR缓存的关键)还有用于存储子组件el的storageContainer,挂载了activate和deactivate方法,在子组件的挂载和卸载时调用,主要用来移动el进行缓存或者复用。由于在keepalive的生命周期中setup只会调用一次,所以不用担心这些数据会被清除覆盖。后续就是setup的返回值处理,因为返回值是一个函数,所以这个函数会被作为keepalive的render函数调用。
keepalive渲染函数做的事情大致是获取子组件vnode,判断vnode是否已经被缓存,如果没有则添加到缓存(这个逻辑是在keepalive mounted或updated生命周期执行的),且vnode的shapFlag会被标记为ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE,组件卸载时会调用keepalive ctx上挂载的deactivate方法,以保存子组件的el;如果已经被缓存,则将缓存子组件vnode的el和component(被缓存的组件实例)赋值给新的子组件vnode且将子组件shapeFlag标记为ShapeFlags.COMPONENT_KEPT_ALIVE,这样在子组件的processComponent中就会判断这个标记然后调用keepalive ctx上挂载的activate方法,将el再移动回现在的父容器,这样就可以避免子组件的重新执行mountComponent重新挂载组件。这样就实现了子组件的缓存。
还有就是关于指定max的LRU缓存的实现,其利用了Set数据结构的插入会记录插入顺序的特性。所以在组件实例缓存被访问时会进行delete再add的操作以更新该组件被记录的顺序,当缓存超出max时keys.values().next().value就是最久没有被激活的缓存,所以删除这个key的缓存即可。