keep-alive实现原理

236 阅读3分钟

keep-alive是什么

keep-alive是vue框架自身的组件,可以在组件切换时,保存其包裹的组件的状态,使其不被销毁,防止多次渲染。其拥有两个独立的生命周期钩子函数 activeddeactived,使用 keep-alive包裹的组件在切换时不会被销毁,而是缓存到内存中并执行 deactived 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

keep-alive的用法

keep-alive的用法也非常简单, 它可以用来包裹动态组件,也可以包裹一个router-view,同时在DOM结构中并不渲染。

keep-alive的应用场景

1. tabs切换时保留切换前的操作
2. 前进刷新后退缓存用户浏览数据浏览位置

keep-alive属性:

1. include:记录的是哪些组件可以被缓存
2. exclude:记录的是哪些组件不可以被缓存
3. max:记录的是可以缓存组件的最大数量

LRU算法介绍

LRU的全称为Least Recently Used。它是一种内存淘汰算法,当内存不够时,将内存中最久没使用的数据清理掉。LRU算法常用于缓存的淘汰策略。

LRU算法原理

为了更好地描述和理解LRU算法的原理,此处我们以一个队列举例,队列的头部表示最久没有被访问的数据,队列的尾部表示最近刚访问的数据。

src=http___img-blog.csdnimg.cn_20190823175601360.png_x-oss-process=image_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lmdGs3NjU3Njg1NDA=,size_16,color_FFFFFF,t_70&refer=http___img.webp

keep-alive封装方法解析

修正缓存


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

在pruneCache方法中我们可以看到当前keep-alive实例缓存 key 数组,并且对于不需要缓存的组件做了及时清除

清除缓存

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

在pruneCacheEntry方法中如果缓存对象 没有实例或者当前缓存的标签不等于当前标签则被强制销毁,同时做清空和删除操作

render函数

 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 {
        // delay setting the cache until update
        this.vnodeToCache = vnode
        this.keyToCache = key
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
  • 获取keep-alive包裹着的第一个子组件对象及其组件名;

  • 根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;

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

  • 在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max的设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key);

  • 最后将该组件实例的keepAlive属性值设置为true;

destroyed

destroyed () { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } },

我们可以看到 在destroyed中 调用了pruneCacheEntry方法,我们再看一下pruneCacheEntry方法。

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)
}
  1. this.cache中缓存的VNode实例;
  2. 遍历调用pruneCacheEntry函数删除缓存VNode还要对应执行组件实例的destory函数;

created

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

created方法就很简单了,他只初始化了两个变量

  1. cache用来缓存虚拟dom;
  2. keys用来缓存虚拟dom的键集合

mounted

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

在mounted这个钩子中对include和exclude参数进行监听,然后实时地更新this.cache对象数据。pruneCache函数的核心也是去调用pruneCacheEntry。

总结

简单的总结下:就是在使用keep-alive时,会根据include和exclude两个属性来判断,keep-alive内部的子组件是否满足匹配条件,如果满足则把子组件对应的虚拟dom的实例利用cache和keys两个属性进行缓存。当下次子组件被重新渲染时首先判断缓存列表中是否存在,如果存在则直接从缓存中拉取,否则暂存在待缓存对象中待下次更新时缓存。