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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容