Keep-Alive 源码试解析

134 阅读2分钟

keep-alive 执行的生命周期

当引入keep-alive的时候,页面第一次进入,钩子的触发顺序created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。
<router-view v-if="$route.meta.keepAlive" style="min-height:100%"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" style="min-height:100%"></router-view>
//不需要刷新的路由配置里面配置 meta: {keepAlive: true}, 这个路由则显示在上面标签;
//需要刷新的路由配置里面配置 meta: {keepAlive: false}, 这个路由则显示在下面标签;

props包含哪三个

  • include 包含的组件(可以为字符串,数组,以及正则表达式,只有匹配的组件会被缓存)

  • exclude 排除的组件(以为字符串,数组,以及正则表达式,任何匹配的组件都不会被缓存)

  • max 缓存组件的最大值(类型为字符或者数字,可以控制缓存组件的个数),最大为10

LRU算法(缓存淘汰算法)

props中的max存在,keep-alive引入了与之搭配的LRU算法
该算法秉承的原则即:如果当前数据被访问过,接下来被访问的几率就会加大。

image.png

  1. 新数据插入到链表尾部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表尾部
  3. 当链表满的时候,将链表头部的数据丢弃。

延伸知识:abstract的作用、拥有该属性的标签有哪些、object.create(null)创建了一个什么对象、vue2中$slots.default获取插槽的默认元素

源码中几个函数的具体用途

1.初始化

export default {
  name: 'keep-alive',
  abstract: true,
  //在组件开头就设置 `abstract` 为 `true`,代表该组件已经被提取出来,不会在页面生成,其子组件会直接挂载到该组件的父组件上。
  props: {
    include: patternTypes,
    exclude: patternTypes,
    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: 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
      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)
        }
      }
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}
```匹配条件通过会进入缓存机制的逻辑,如果命中缓存,从 `cache` 中获取缓存的实例设置到当前的组件上,并调整 `key` 的位置将其放到最后。如果没命中缓存,将当前 `VNode` 缓存起来,并加入当前组件的 `key`。如果缓存组件的数量超出 `max` 的值,即缓存空间不足,则调用 `pruneCacheEntry` 将最旧的组件从缓存中删除,即 `keys[0]` 的组件。之后将组件的 `keepAlive` 标记为 `true`,表示它是被缓存的组件。


```// 遍历缓存表
function pruneCache(filter?: (name: string) => boolean) {
  cache.forEach((vnode, key) => {
    const name = getComponentName(vnode.type as ConcreteComponent);
    if (name && (!filter || !filter(name))) {
      // !filter(name) 即 name 在 includes 或不在 excludes 中
      pruneCacheEntry(key);
    }
  });
}
// 依据 key 值从缓存表中移除对应组件
function pruneCacheEntry(key: CacheKey) {
  const cached = cache.get(key) as VNode;
  if (!current || cached.type !== current.type) {
    /* 当前没有处在 activated 状态的组件
     * 或者当前处在 activated 组件不是要删除的 key 时
     * 卸载这个组件
    */
    unmount(cached); // unmount方法里同样包含了 resetShapeFlag
  } else if (current) {
    // 当前组件在未来应该不再被 keepAlive 缓存
    // 虽然仍在 keepAlive 的容量中但是需要刷新当前组件的优先级
    resetShapeFlag(current);
    // resetShapeFlag 
  }
  cache.delete(key);
  keys.delete(key);
}
function resetShapeFlag(vnode: VNode) {
  let shapeFlag = vnode.shapeFlag; // shapeFlag 是 VNode 的标识
   // ... 清除组件的 shapeFlag
}

实现代码展示

1.route字段中添加keepalive,true即为缓存路由,否则不缓存

const route = {
                path: menuList[i].url,
                component:
                    routeAllPathToCompMap[`../views/${menuList[i].url}/index.vue`],
                name: menuList[i].url,
                meta: {
                    title: menuList[i].name,
                    menuId: menuList[i].menuId,
                    keepAlive:true
                }
            }

2.在页面使用keepalive

        <RouterView v-slot="{ Component }">
          <KeepAlive :include="keepAliveComponents">
            <component :is="Component" :key="route.fullPath" />
          </KeepAlive>
        </RouterView>

keepalive实现的逻辑梳理

  • 使用 LRU 缓存机制进行缓存,max 限制缓存表的最大容量

  • 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

  • 根据组件 ID 和 tag 生成缓存 Key ,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

  • 获取节点名称,或者根据节点 cid 等信息拼出当前 组件名称 ( 因此给各个组件标记各自名称这件事变得尤为重要 )

  • 获取 keep-alive 包裹着的第一个子组件对象及其组件名