[Vue 源码] Vue 3.2 - KeepAlive 原理

178 阅读3分钟

代码运行结果

keep-alive现象.gif

代码示例

      const c1 = {
        name: "c1",
        setup() {
          return () => {
            console.log('c1 渲染')
            return h('div', null, 'c1')
          }
        }
      }

      const c2 = {
        name: "c2",
        setup() {
          return () => {
            console.log('c2 渲染')
            return h('div', null, 'c2')
          }
        }
      }

      render(h(KeepAlive, null, { default: () => h(c1) }), app)

      setTimeout(() => {
        render(h(KeepAlive, null, { default: () => h(c2) }), app)
      }, 2000)

      setTimeout(() => {
        render(h(KeepAlive, null, { default: () => h(c1) }), app)
      }, 3000)

第一:第一次 KeepAlive 组件挂载的时候,还是走正常逻辑,组件挂载可以看之前的这篇文章, # [Vue 源码] Vue 3.2 - 组件挂载原理 这里我们直接到执行 setup 函数,及其返回的 render 函数流程。

  • 执行 setup 函数。
    • const keys: Keys = new Set()存储缓存组件的 key, 如果没有就存储 组件的 name。

    • const cache: Cache = new Map() 存储缓存 KeepAlive 组件 name 和 subTree 的映射 (也就是组件 rener函数/vue 模板的虚拟dom)

    • onMounted(cacheSubtree) 挂载的时候去收集 subTree 和 Name 映射。本案例是 c1 -> c1 的subTree.

    • onUpdated(cacheSubtree) 更新的时候去收集 subTree 和 Name 映射。

    • 初始化 activate 和 deactivate 方法,前者是 激活 KeepAlive 组件方法,后者是 KeepAlive 卸载的失活方法。

    • 从组件示例的 ctx 拿到 renderer 渲染器,上面定义了一些供 keepAlive 操作 Dom 的一些方法。这些方法是在第一次挂载 keepAlive 组件时挂载的。

    // mountComponent 这些方法是在第一次挂载 keepAlive 组件时挂载的。
    // inject renderer internals for keepAlive
    if (isKeepAlive(initialVNode)) {
      ;(instance.ctx as KeepAliveContext).renderer = internals
    }

// 定义了一些供 keepAlive 操作 Dom 的一些方法
const {
      renderer: {
        p: patch,
        m: move,
        um: _unmount,
        o: { createElement }
      }
    } = sharedContext
  • 执行 setup 函数返回的 render 函数。
    • keepAlive 组件的 children 会被编译成组件的 default 插槽。render(h(KeepAlive, null, { default: () => h(c1) }), app)
    • 渲染的时候通过 const children = slots.default(),取出默认插槽进行渲染。

至此第一次挂载完毕,页面上显示出了 c1.并在控制台打印出 c1 渲染。

  • 两秒过后,再次渲染 c2 的 KeepAlive 组件,先执行 c1 的 unmout 方法,keepAlive 组件 unmount 的时 候,会调用 deactivate 方法.
 sharedContext.deactivate = (vnode: VNode) => {
      const instance = vnode.component!
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
 }

  • 在 deactivate 中将 c1 的 dom 并没有被卸载/remoe, 而是缓存在了 storageContainer 这个 dom 中。
  • storageContainer 就是在 KeepAlive 中创建的 div, const storageContainer = createElement('div').
  • c1 从页面上消失,因为已经把 subTree 移到了 storageContainer 中。
  • 开始渲染 c2 KeepAlive 组件, setup 函数 -> render 函数, 再将 c2 和 对应的subTree 放入缓存中去
  • 执行 c2 的 render 函数,打印 c2 渲染,返回虚拟dom,patch 更新插槽之后完成渲染。

自此 c2 被渲染到了页面上。

  • 过了一秒后,再次渲染 c1 的 KeepAlive 组件,先执行 c2 的 unmout 方法,keepAlive 组件 unmount 的时 候,会调用 deactivate 方法.
  • 在 deactivate 中将 c2 的 dom 并没有被卸载/remoe, 而是还存在了 storageContainer 这个 dom 中。
  • storageContainer 就是在 KeepAlive 中创建的 div, const storageContainer = createElement('div').
  • c2 从页面上消失,因为已经把 subTree 移到了 storageContainer 中。
  • 开始渲染 c1 KeepAlive 组件,发现 c1 已经被缓存过了, 给 KeepAlive innerChild 孩子打上COMPONENT_KEPT_ALIVE 标记 。
if (cachedVNode) {
        // copy over mounted state
        vnode.el = cachedVNode.el
        vnode.component = cachedVNode.component
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE       
}
  • 由于 KeepALive 组件的第一个孩子 vnode 的 shapeFlag 打上了缓存标记,所以走 active 逻辑。
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } 
  • 在 active 中将 subTree 指向的 dom 引用,从 storageContainer 缓存容器中移动了回来。并没有触发 render 函数,这时候界面上已经 有 c1 了。
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      const instance = vnode.component!
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)

自此 KeepAlive 缓存组件渲染完毕!

核心原理

KeepAlive 中的核心原理,就是 dom 并没有被卸载/remoe, 而是缓存在了 storageContainer 这个 dom 中。希望以下的这段代码可以给读者更多的启示和思考。

当然 KeepAlive 中的 LRU 最近最少使用算法,也值得读者去阅读和探索。

    <div id="app"></div>
    <div id="storage"></div>
    <script>
      let divItem = document.createElement('div')
      divItem.innerHTML = 'app'

      let obj = { item: divItem }

      storage.appendChild(obj.item)

      setTimeout(() => {
        app.appendChild(obj.item)
      }, 2000)
    </script>

keep-alive 原理.gif