vue2源码笔记之keep-alive

395 阅读3分钟

概念

在使用vue时,组件之间切换,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。

我们可以用一个 <keep-alive> 元素将其动态组件包裹起来。

<!-- 失活的组件将会被缓存!-->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

它接收三个参数:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。 <keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。

源码实现

export default {
  name: 'keep-alive',
  abstract: true,
  props: {
    include: [String, RegExp, Array], // 包含 可传字符串、正则表达式、数组
    exclude: [String, RegExp, Array], // 排除
    max: [String, Number]             // 最多可以缓存多少组件实例
  },
  created () {
    this.cache = Object.create(null)  // 初始化 缓存对象
    this.keys = []
  },
  destroyed () {
    // 页面销毁 缓存也清空
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },
  mounted () {
    // 组件初始化 剪枝
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
  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 (
        // 没有包括在内
        (include && (!name || !matches(include, name))) ||
        // excluded 排除的
        (exclude && name && matches(exclude, name))
      ) {
        // 直接返回节点
        return vnode
      }

      const { cache, keys } = this
      //  同一个构造函数可能被注册为不同的局部组件
      // 所以只有cid是不够的 (#3269)
      const key = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      // LRU算法
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // 使当前键最新鲜 先刪再加入, 保证尾部最新
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        // 删除最老的条目
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    // 返回
    return vnode || (slot && slot[0])
  }
}

代码很简单,就是初始化一个对象用来缓存组件节点,根据传进来的参数判断哪些组件是否要缓存,如果不需要缓存,则直接返回节点, 否则读取缓存中的节点。

在缓存超过max时,使用了缓存淘汰算法LRU,其实就是删了再加上,超过就删掉缓存头部节点。

其中的工具函数

// 获取组件名字
function getComponentName (opts) {
  return opts && (opts.Ctor.options.name || opts.tag)
}
// 匹配
function matches (pattern, name){
  if (Array.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
}
// 修剪缓存
function pruneCache (keepAliveInstance, filter) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode = cache[key]
    if (cachedNode) {
      const name = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}
// 修剪缓存入口
function pruneCacheEntry ( cache, key, keys, current) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}
// 是否是正则类型
export function isRegExp (v) {
  return _toString.call(v) === '[object RegExp]'
}

/**从数组中删除项
 * Remove an item from an array
 */
 export function remove (arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}
// 获取第一个 组件
export function getFirstComponentChild (children) {
  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      const c = children[i]
      if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
        return c
      }
    }
  }
}

export function isAsyncPlaceholder (node) {
  return node.isComment && node.asyncFactory
}

export function isDef (v) {
  return v !== undefined && v !== null
}

那么keep-alive是如何触发这两个activateddeactivated钩子的呢? 在组件最后我们可以看到

vnode.data.keepAlive = true

在源码中通过搜索keepAlive可以定位到这些代码:

image.png

keepAlive为true,它会调一个方法 image.png

依着这个方法继续找 image.png

好像快找到了, 这里调用所有active钩子 image.png 最后找到了

image.png

也就是说,通过往实例上设置个属性keepAlive来判断是否触发activated钩子。