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

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就是根据名称寻找局部或者全局注册的组件,然后渲染对应的组件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值