vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > vue3 keep-alive和动态组件的实现逻辑

解读Vue3中keep-alive和动态组件的实现逻辑

作者:JonnyLan

这篇文章主要介绍了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

渲染函数中获取子树节点VNode的key, 缓存cache中查看是否有key对应的缓存节点

const key = vnode.key
const cachedVNode = cache.get(key)

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

记录下点前的key

pendingCacheKey = key

如果有找到缓存的cachedVNode节点,将缓存的cachedVNode节点的组件实例和节点元素 复制给新的VNode节点。没有找到就先将当前子树节点VNode的pendingCacheKey加入到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)
}

在组件挂载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-alive的patch复用逻辑

我们知道生成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-alive的activate方法;

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-alive的deactivate方法。

总结: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)
}

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

问题:旧节点的deactivate和新节点的active谁先执行

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

keep-alive的unmount逻辑

将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,exclude和max

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

动态组件

使用方法

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

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文