探究Vue3的keep-alive和动态组件的实现逻辑

2,189 阅读6分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

keep-alive组件是Vue提供的组件,它可以缓存组件实例,在某些情况下避免了组件的挂载和卸载,在某些场景下非常实用。

例如最近我们遇到了一种场景,某个组件上传较大的文件是个耗时的操作,如果上传的时候切换到其他页面内容,组件会被卸载,对应的下载也会被取消。此时可以用keep-alive组件包裹这个组件,在切换到其他页面时该组件仍然可以继续上传文件,切换回来也可以看到上传进度。

keep-alive

渲染子节点
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,

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

    // 需要渲染的子树VNode
    let current: VNode | null = null

    return () => {

      // 获取子节点, 由于Keep-alive只能有一个子节点,直接取第一个子节点
      const children = slots.default()
      const rawVNode = children[0]

      // 标记 | ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE,这个组件是`keep-alive`组件, 这个标记 不走 unmount逻辑,因为要被缓存的
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      // 记录当前子节点
      current = vnode

      // 返回子节点,代表渲染这个子节点
      return rawVNode
    }
  }
}

组件的setup返回函数,这个函数就是组件的渲染函数; keep-alive是一个虚拟节点不需要渲染,只需要渲染子节点,所以函数只需要返回子节点VNode就行了。

缓存功能
  • 定义存储缓存数据的Map, 所有的缓存键值数组Keys,代表当前子组件的缓存键值pendingCacheKey
const cache = new Map()
const keys: Keys = new Set()
let pendingCacheKey: CacheKey | null = null
  • 渲染函数中获取子树节点VNodekey, 缓存cache中查看是否有key对应的缓存节点
const key = vnode.key
const cachedVNode = cache.get(key)

key是生成子节点的渲染函数时添加的,一般情况下就是0,1,2,...这些数字。

  • 记录下点前的key
pendingCacheKey = key
  • 如果有找到缓存的cachedVNode节点,将缓存的cachedVNode节点的组件实例和节点元素 复制给新的VNode节点。没有找到就先将当前子树节点VNodependingCacheKey加入到Keys中。
if (cachedVNode) {
  // 复制节点
  vnode.el = cachedVNode.el
  vnode.component = cachedVNode.component
  // 标记 | ShapeFlags.COMPONENT_KEPT_ALIVE,这个组件是复用的`VNode`, 这个标记 不走 mount逻辑
  vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
} else {
  // 添加 pendingCacheKey
  keys.add(key)
}

问题: 这里为什么不实现在cache中存入{pendingCacheKey: vnode}呢? 答案: 这里其实可以加入这逻辑,只是官方间隔这个逻辑延后实现了, 我觉得没什么差别。

  • 在组件挂载onMounted和更新onUpdated的时候添加/更新缓存
onMounted(cacheSubtree)
onUpdated(cacheSubtree)

const cacheSubtree = () => {
  if (pendingCacheKey != null) {
    // 添加/更新缓存
    cache.set(pendingCacheKey, instance.subTree)
  }
}

全部代码
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,

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

    let current: VNode | null = null
    // 缓存的一些数据
    const cache = new Map()
    const keys: Keys = new Set()
    let pendingCacheKey: CacheKey | null = null

    // 更新/添加缓存数据
    const cacheSubtree = () => {
      if (pendingCacheKey != null) {
        // 添加/更新缓存
        cache.set(pendingCacheKey, instance.subTree)
      }
    }

    // 监听生命周期
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

    return () => {
      const children = slots.default()
      const rawVNode = children[0]

      // 获取缓存
      const key = rawVNode.key
      const cachedVNode = cache.get(key)

      pendingCacheKey = key

      if (cachedVNode) {
        // 复用DOM和组件实例
        rawVNode.el = cachedVNode.el
        rawVNode.component = cachedVNode.component
      } else {
        // 添加 pendingCacheKey
        keys.add(key)
      }

      rawVNode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
      current = rawVNode
      return rawVNode
    }
  }
}

至此,通过cache实现了DOM组件实例的缓存。

keep-alivepatch复用逻辑

我们知道生成VNode后是进行patch逻辑,生成DOM

const processComponent = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  n2.slotScopeIds = slotScopeIds
  if (n1 == null) {
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      ;(parentComponent!.ctx as KeepAliveContext).activate(
        n2,
        container,
        anchor,
        isSVG,
        optimized
      )
    } else {
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  }
}

processComponent处理组件逻辑的时候如果是复用ShapeFlags.COMPONENT_KEPT_ALIVE则走的父组件keep-aliveactivate方法;

const unmount: UnmountFn = (
  vnode,
  parentComponent,
  parentSuspense,
  doRemove = false,
  optimized = false
) => {
  const {
    type,
    props,
    ref,
    children,
    dynamicChildren,
    shapeFlag,
    patchFlag,
    dirs
  } = vnode
  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
    return
  }
}

unmount卸载的keep-alive组件ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE时调用父组件keep-alivedeactivate方法。

总结:keep-alive组件的复用和卸载被activate方法和deactivate方法接管了。

active逻辑
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
  const instance = vnode.component!
  // 1. 直接挂载DOM
  move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
  // 2. 更新prop
  patch(
    instance.vnode,
    vnode,
    container,
    anchor,
    instance,
    parentSuspense,
    isSVG,
    vnode.slotScopeIds,
    optimized
  )
  // 3. 异步执行onVnodeMounted 钩子函数
  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)

}
  1. 直接挂载DOM
  2. 更新prop
  3. 异步执行onVnodeMounted钩子函数
deactivate逻辑
const storageContainer = createElement('div')

sharedContext.deactivate = (vnode: VNode) => {
  const instance = vnode.component!
  // 1. 把DOM移除,挂载在一个新建的div下
  move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
  // 2. 异步执行onVnodeUnmounted钩子函数
  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)
  }
}
  1. DOM移除,挂载在一个新建的div
  2. 异步执行onVnodeUnmounted钩子函数

问题:旧节点的deactivate和新节点的active谁先执行 答案:旧节点的deactivate先执行,新节点的active后执行。

keep-aliveunmount逻辑
  • cache中出当前子树VNode节点外的所有卸载,当前组件取消keep-alive的标记, 这样当前子树VNode会随着keep-alive的卸载而卸载。
onBeforeUnmount(() => {
  cache.forEach(cached => {
    const { subTree, suspense } = instance
    const vnode = getInnerChild(subTree)
    if (cached.type === vnode.type) {
      // 当然组件先取消`keep-alive`的标记,能正在执行unmout
      resetShapeFlag(vnode)
      // but invoke its deactivated hook here
      const da = vnode.component!.da
      da && queuePostRenderEffect(da, suspense)
      return
    }
    // 每个缓存的VNode,执行unmount方法
    unmount(cached)
  })
})

<!-- 执行unmount -->
function unmount(vnode: VNode) {
    // 取消`keep-alive`的标记,能正在执行unmout
    resetShapeFlag(vnode)
    // unmout
    _unmount(vnode, instance, parentSuspense)
}

keep-alive卸载了,其缓存的DOM也将被卸载。

keep-alive缓存的配置include,excludemax

这部分知道逻辑就好了,不做代码分析。

  1. 组件名称在include中的组件会被缓存;
  2. 组件名称在exclude中的组件不会被缓存;
  3. 规定缓存的最大数量,如果超过了就把缓存的最前面的内容删除。

动态组件

使用方法
<keep-alive>
  <component is="A"></component>
</keep-alive>
渲染函数
resolveDynamicComponent("A")
resolveDynamicComponent的逻辑
export function resolveDynamicComponent(component: unknown): VNodeTypes {
  if (isString(component)) {
    return resolveAsset(COMPONENTS, component, false) || component
  }
}

function resolveAsset(
  type,
  name,
  warnMissing = true,
  maybeSelfReference = false
) {
  const res =
    // local registration
    // check instance[type] first which is resolved for options API
    resolve(instance[type] || Component[type], name) ||
    // global registration
    resolve(instance.appContext[type], name)
  return res
}

指令一样,resolveDynamicComponent就是根据名称寻找局部或者全局注册的组件,然后渲染对应的组件。