keep-alive 的实现原理

技术 8月 04, 2020

基本用法

<keep-alive>
-  <div>123</div>
+  <component :is="switchOne ? 'TabOne' : 'TabTwo'" />
-  <component :is="switchOne ? 'TabTwo' : 'TabOne'" />
</keep-alive>
  • keep-alive 只会去缓存 第一级第一个 组件
  • 我们可以通过 include / exclude / max 来对缓存进行更小颗粒的控制
  • 缓存的组件在切换的时候会触发 activated / deactivated 的生命周期函数

组件实现原理

export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    // 创建缓存队列以及其对应的 key 值队列
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    // 销毁所有已经保存的组件实例
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    // 监听 include / exclued,用于让缓存队列保持一致性
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    // 从 $slot 中获取 keep-alive 的 children
    const slot = this.$slots.default
    // 找到第一个组件的最新的 VNode
    const vnode: VNode = getFirstComponentChild(slot)
    // 获取其配置信息
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // 被捕获的组件名称
      const name: ?string = getComponentName(componentOptions)
      // 如果不在缓存的目标范围内,则直接返回新的 VNode
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }
			
      const { cache, keys } = this
      // 获得组件对应的 key 值
      const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      
      // LRU 缓存操作,如果缓存中存在对应的实例,则将实例注入到 VNode 中
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      // 标注 keepAlive,方便 Vue 配合
      vnode.data.keepAlive = true
    }
    
    // 返回
    return vnode || (slot && slot[0])
  }
}

缓存核心操作的内容其实是 componentInstance,如果已经初始化过对应的实例则不用初始化,直接取出即可。

Vue 内部是如何配合的


先明确一下正常组件实例化的过程:

  • AST(编译 template)
  • CodeGen(生成 render 函数)
  • VNode(通过 render 获得组件 VNode,并将组件的构造函数存到 VNode 中)
  • Patch
    • 新增节点(通过 VNode 中的构造函数初始化组件)
    • 更新节点(基于旧的组件实例更新其状态)
    • 删除节点


如果没有 keep-alive,组件在进行切换到时候,Vue 在 patch 会认为是新增的节点而重新创建组件实例,从而导致先前可能存在状态会被清空。
如果有 keep-alive,切换节点操作最后 patch 还是会走到 **新增节点 **的逻辑,但是不一样的是,新的 VNode 中已经有组件实例,同时 VNode.data.keepAlive 标识了是一个 keep-alive 下的组件。


下面是新增组件的函数

  • 这里主要是调用 VNode 的 init()
  • 同时实例化完后会决定是否调用重新激活(activated)生命周期函数
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // 标记是否需要重新激活
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    // 调用 init 钩子函数
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
   
    // 如果组件实例构建成功
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      
      // 如果是重新激活的组件则调用对应的钩子函数
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

这里是 keep-alive 与普通组件初始化开始走向分岔路的起点

  • 如果是 keep-alive 下的组件,同时初始化过了,则直接 patch
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  // 如果已经有组件实例 并且 没被销毁 并且有 keepAlive 则走 patch
  // 反之走初始化
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    // kept-alive components, treat as a patch
    const mountedNode: any = vnode // work around flow
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
},

Pengsha Ying

逝者如斯,故不舍昼夜