keep-alive 原理解析

219 阅读4分钟

keep-alive

用法

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。
<keep-alive include='myname' exclude='exmyname' :max='10'>
    <comp-a/>
</keep-alive>

_getComponentName

  • 判断是否传入组件实例 获取组件的属性值 name 如果为存在组件属性则使用组件注册的标签来命名缓存的 key
function _getComponentName(opts?: VNodeComponentOptions): string | null {
  return opts && (getComponentName(opts.Ctor.options as any) || opts.tag)
}

matches

  • 判断当前 includeexclude 的类型 这两个属性只支持 Object | String | Array 类型
function matches(
  pattern: string | RegExp | Array<string>,
  name: string
): boolean {
  if (isArray(pattern)) {
    return pattern.indexOf(name) > -1
  } else if (typeof pattern === 'string') {
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

pruneCache

  • pruneCache 删除缓存接受一个 keepAlive实例和一个filter函数,首先从实例中取出 cache、keys和对应的 vnode。然后遍历整个 cache 对象,如果当前组件实例在缓存中并且参数合法,就执行 pruneCacheEntry 方法
function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const entry: ?CacheEntry = cache[key]
    if (entry) {
      const name: ?string = entry.name
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

mounted

  • mounted 生命周期 对props 参数进行了 侦听处理 里面调用了 pruneCache 方法
mounted () {
    this.cacheVNode()
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
}

pruneCacheEntry

  • 对传入的组件实例进行判断 是否存 cache 缓存对象中或者缓存标签名称不一样说明组件实例已经失效,销毁组件,直接从 cache 中移除组件的实例
function pruneCacheEntry(
  cache: CacheEntryMap,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const entry = cache[key]
  if (entry && (!current || entry.tag !== current.tag)) {
    // @ts-expect-error can be undefined
    entry.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

remove

  • 全局组件方法 remove
/**
 * Remove an item from an array.
 */
export function remove(arr: Array<any>, item: any): Array<any> | void {
  const len = arr.length
  if (len) {
    // fast path for the only / last item
    if (item === arr[len - 1]) {
      arr.length = len - 1
      return
    }
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

cacheVNode

  • 该方法添加缓存方法 判断 vnodeToCache 值是否存在组件信息 通过 keyToCache 值缓存组件的key vnodeToCache 缓存组件的消息 完成后销毁当前组件信息
  • 判断 this.max && (keys.length > parseInt(this.max)) keys.length 大于 max 缓存组件数量 LRU 策略 删除第一项 (最久未使用的一项)
 methods: {
    cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      if (vnodeToCache) {
        const { tag, componentInstance, componentOptions } = vnodeToCache
        cache[keyToCache] = {
          name: _getComponentName(componentOptions),
          tag,
          componentInstance
        }
        keys.push(keyToCache)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
   }
},

created

  • 初始化两个变量
  • cache 缓存的对象
  • keys 缓存的 key 数组
created() {
    this.cache = Object.create(null)
    this.keys = []
 },

destroyed

  • 生命周期 清空所有的缓存组件
destroyed() {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
}

updated

  • 组件更新时触发 添加缓存组件的方法
 updated() {
    this.cacheVNode()
 },

render

    1. 通过 this.$slots.default 获取组件的插槽内容
    1. getFirstComponentChild 获取第一个子组件的虚拟dom Vnode
    1. 判断传入的参数 max include exclude 如果不满足 return Vnode
    1. 如果组件缓存存在 根据 LRU策略移动组件在缓存数组中的位置 如果缓存不存在则 vnodeToCache = vnode 绑定其关系触发更新添加到缓存项中
    1. 完成后标记 vnode.data.keepAlive = true
render() {
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    const componentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name = _getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included 不包含
        (include && (!name || !matches(include, name))) ||
        // excluded 排除在外
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            // 相同的构造函数可能会注册为不同的本地组件
            // 所以只有 cid 是不够的 (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : '')
          : vnode.key
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest 使当前密钥最新鲜
        remove(keys, key)
        keys.push(key)
      } else {
        // delay setting the cache until update 延迟设置缓存直到更新
        this.vnodeToCache = vnode
        this.keyToCache = key
      }

      // @ts-expect-error can vnode.data can be undefined
      // @ts-expect-error 可以 vnode.data 可以是未定义的
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
}

为什么被缓存的组件不会执行 created 和 mounted 呢?

  • createComponent 方法中 i = i.init 方法
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef((i = i.hook)) && isDef((i = i.init))) {
        i(vnode, false /* hydrating */)
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      // 在调用 init 钩子之后,如果 vnode 是一个子组件
      // 它应该创建一个子实例并挂载它。 孩子
      // 组件还设置了占位符 vnode 的 elm。
      // 在这种情况下,我们可以只返回元素并完成。
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
    }
  }
}
  • 满足 vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive 就不会执行初渲染方法 $mount
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
    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)
    }
  },

  • prepatch 更新了梓组件 相当于从缓存里取出了当前组件实例
 prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = (vnode.componentInstance = oldVnode.componentInstance)
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

createComponent 渲染 dom

  • isReactivated 判断当前组件为缓存的组件
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef((i = i.hook)) && isDef((i = i.init))) {
        i(vnode, false /* hydrating */)
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }
  • reactivateComponent 循环执行缓存的组件生命周期
function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i
    // hack for #4339: a reactivated component with inner transition
    // does not trigger because the inner node's created hooks are not called
    // again. It's not ideal to involve module-specific logic in here but
    // there doesn't seem to be a better way to do it.
    // hack for #4339: 具有内部过渡的重新激活的组件
    // 不会触发,因为内部节点创建的钩子没有被调用
    // 再次。 在这里涉及特定于模块的逻辑并不理想,但是
    // 似乎没有更好的方法来做到这一点。
    let innerNode = vnode
    while (innerNode.componentInstance) {
      innerNode = innerNode.componentInstance._vnode
      if (isDef((i = innerNode.data)) && isDef((i = i.transition))) {
        for (i = 0; i < cbs.activate.length; ++i) {
          cbs.activate[i](emptyNode, innerNode)
        }
        insertedVnodeQueue.push(innerNode)
        break
      }
    }
    // unlike a newly created component,
    // a reactivated keep-alive component doesn't insert itself
    // 与新创建的组件不同,
    // 重新激活的 keep-alive 组件不会插入自身
    insert(parentElm, vnode.elm, refElm)
  }
  • insert 方法执行替换当前的dom
function insert(parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
    }
  }

keep-alive 如何对包裹的组件进行缓存呢?

  • 首次加载被包裹组件时,由 keep-alive.js 中的 render 函数可知,vnode.componentInstance 的值 是undefined,keepAlive 的值 是true,因为keep-alive组件作为父组件,它的render函数会先于被包裹组件执行;那么就只执行到 i(vnode, false /* hydrating */),后面的逻辑不再执行;
  • 再次访问被包裹组件时,vnode.componentInstance的值就是已经缓存的组件实例,那么会执行 insert(parentElm, vnode.elm, refElm)逻辑,这样就直接把上一次的DOM插入到了父元素中。

文章参考 : bbchin.com/archives/so…