vue的keep-alive源码分析

660 阅读3分钟

概述

Vue里面,keep-alive 是一个内置的抽象组件,它的作用是缓存所包裹组件的实例。

源码分析

源码地址:src/core/components/keep-alive.js

官方参数解释

Props

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。

<keep-alive :include="cachedViews">
  <router-view :key="key" />
</keep-alive>

从生命周期开始

既然 keep-alive 是一个组件,那么就会有组件的生命周期,接下来就根据这个顺序来讲述。

created () {
  this.cache = Object.create(null)
  this.keys = []
}

首先在创建完组件实例的时候会初始化两个参数,keys 存放组件实例对应的key,而 cache 则是相当于一个缓存池(代码层面就是一个对象),后面部分讲述的会更容易理解。

缓存方法

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
      }
    }

mounted

初始化后,可以看到 mounted 里添加了 watch 监听两个参数 includeexclude ,这是为了匹配参数然后删除不需要的缓存。如下:

mounted () {
  this.cacheVNode()
  this.$watch('include', val => {
    pruneCache(this, name => matches(val, name))
  })
  this.$watch('exclude', val => {
    pruneCache(this, name => !matches(val, name))
  })
}

匹配的方式比较简单,主要通过三种方式去匹配组件名称,分别为字符串、正则表达式,字符串数组。代码如下:

/* 通过正则匹配组件名称 */
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  // 数组
  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: 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
      // filter 和 matches相呼应
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

/** 删除具体的记录,并销毁对象 */
function pruneCacheEntry (
  cache: CacheEntryMap,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const entry: ?CacheEntry = cache[key]
  if (entry && (!current || entry.tag !== current.tag)) {
    entry.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

以上可以看出 pruneCache 方法主要是遍历缓存池,匹配上的再调用 pruneCacheEntry 方法,实际上的删除操作在后者,这里需要注意的是删除缓存前需要把该组件实例销毁 cached.componentInstance.$destroy()

render

核心的逻辑处理在 render 方法里

render () {
    // 插槽
    const slot = this.$slots.default
    // 获取第一个组件vnode对象
    const vnode: VNode = getFirstComponentChild(slot)

    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions

    if (componentOptions) {
      // check pattern
      const name: ?string = 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: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      // 判断当前key是否存在缓存中,如果存在则更新数组,这里用到了LRU算法
      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
      }

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

简单梳理下逻辑:

  • 第一步,找到包裹的组件虚拟dom节点vnode。
  • 第二步,判断是否需要缓存,如果不需要则直接返回。
  • 第三步,需要缓存的情况,更新缓存池。如果缓存池已存在该vnode就直接复用,然后把 keykeys 里删掉,再把 key pushkeys(其实就是使用了LRU算法),如果不存在,则把这vnodekey留着下次用。

总结

  • keep-alive 是个抽象组件,所以也有生命周期
  • keep-alive 是通过keyscache两个变量来管理缓存的,可以把keys看成版本号管理,cache 则是组件缓存池
  • 如果与 key 对应的缓存已经存在则直接复用,再更新一把 keys