Vue中keep-alive原理

156 阅读7分钟

Vue中keep-alive原理

定义

keep-aliveVue中内置的一个抽象组件。它自身不会渲染一个DOM元素,也不会出现在父组件链中。当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

keep-alive是用来缓存组件的,比如我们有个列表页,在点击详情页之后,如果返回之后不想刷新列表页,就可以用keep-alive组件进行缓存。除此以外,还有很多应用场景。

用法

用法1:

我们想要缓存某个组件,只要用keep-alive组件将其包裹就行。

<keep-alive>
    <component></component>
</keep-alive>

用法2:

包裹component组件缓存动态组件,或者包裹router-view缓存路由页面,也就是keep-alive配合路由守卫(元信息)实现缓存。

比如常在router.js路由表里定义好哪些页面需要缓存,就可以通过下面这样实现了:

{
	path: "/index",
	name: 'index',   
	component: () => import(/* webpackChunkName: "index" */ '@/pages/index'),
	meta: {
		title: '首页', 
		keepAlive: true
	}
}
<keep-alive>
      <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive && isRouterAlive"></router-view>

属性

  • include - 逗号分隔字符串或正则表达式或一个数组来表示。只有名称匹配的组件会被缓存。
  • exclude - 逗号分隔字符串或正则表达式或一个数组来表示。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>
 
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>
 
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

注意:想要缓存的组件一定要给定name属性,并且要和include,exclude给定的值一致

生命周期钩子

keep-alive提供了两个生命钩子,分别是activateddeactivated

因为keep-alive会将组件保存在内存中,并不会销毁以及重新创建,所以不会重新调用组件的created等方法,需要用activated与deactivated这两个生命钩子来得知当前组件是否处于活动状态。

组件一旦被keep-alive缓存,那么再次渲染的时候就不会执行 created、mounted 等钩子函数。使用keep-alive组件后,被缓存的组件生命周期会多activated和deactivated 两个钩子函数,它们的执行时机分别是keep-alive包裹的组件激活时调用和停用时调用。

源码:

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)
    /* 获取该组件节点的componentOptions */
    const componentOptions = vnode && vnode.componentOptions
 
    if (componentOptions) {
      /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
      const name = getComponentName(componentOptions)
 
      const { include, exclude } = this
      /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
      if (
        (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)
        ? 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])
  }
}

可以看到,它有3个属性,即有3个props。此外,它有createddestroyedmountedrender四个钩子。

分析

created和destroyed钩子

  • created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。
  • destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。
created () {
 	/* 缓存对象 */
    this.cache = Object.create(null)
    this.keys = []
},
/* destroyed钩子中销毁所有cache中的组件实例 */
destroyed () {
    for (const key in this.cache) {
    	pruneCacheEntry(this.cache, key, this.keys)
    }
}

render钩子

keep-alive实现缓存的核心代码就在这个钩子函数里。

  • 先获取到插槽里的内容
  • 调用getFirstComponentChild方法获取第一个子组件,获取到该组件的name,如果有name属性就用name,没有就用tag名。
/* 获取该组件节点的名称 */
const name = getComponentName(componentOptions)
 
/* 优先获取组件的name字段,如果name不存在则获取组件的tag */
function getComponentName (opts: ?VNodeComponentOptions): ?string {
	return opts && (opts.Ctor.options.name || opts.tag)
}
  • 接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回这个组件的 vnode(vnode是一个VNode类型的对象),否则的话走下一步缓存。匹配:
/* 检测name是否匹配 */
function matches (pattern: string | RegExp, name: string): boolean {
  if (typeof pattern === 'string') {
    /* 字符串情况,如a,b,c */
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    /* 正则 */
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

检测include与exclude属性匹配的函数很简单,include与exclude属性支持字符串如"a,b,c"这样组件名以逗号隔开的情况以及正则表达式。matches通过这两种方式分别检测是否匹配当前组件。

const { include, exclude } = this
/* 如果name与include规则不匹配或者与exclude规则匹配则表示不缓存,直接返回vnode */
if (
    (include && (!name || !matches(include, name))) ||
    // excluded
    (exclude && name && matches(exclude, name))
) {
    return vnode
}
  • 缓存机制:接下来的事情很简单,根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。最后返回vnode(有缓存时该vnode的componentInstance已经被替换成缓存中的了)。缓存的处理:
/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    /* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */
    remove(keys, key)
    keys.push(key)
} 
/* 如果没有命中缓存,则将其设置进缓存 */
else {
    cache[key] = vnode
    keys.push(key)
    /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
    if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
}
/* 最后设置keepAlive标记位 */
vnode.data.keepAlive = true

命中缓存时会直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个。

如果没有命中缓存,即该组件还没被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys中。此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉。

为什么要删除第一个缓存组件并且为什么命中缓存了还要调整组件key的顺序?这其实应用了一个缓存淘汰策略LRU: LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

this.keys的逻辑:

将新数据从尾部插入到this.keys中 每当缓存命中(即缓存数据被访问),则将数据移到this.keys的尾部 当this.keys满的时候,将头部的数据丢弃

mounted钩子

  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
watch: {
    /* 监视include以及exclude,在被修改的时候对cache进行修正 */
    include (val: string | RegExp) {
        pruneCache(this.cache, this._vnode, name => matches(val, name))
    },
    exclude (val: string | RegExp) {
        pruneCache(this.cache, this._vnode, name => !matches(val, name))
    }
},

如果include 或exclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下:

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

在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象删除即可。

总结

Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。

Vue keep-alive后不能触发destroyed

  • 不设置keep-alive,离开当前路由时,会调用beforeDestroydestroyed来销毁。
  • 当设置keep-alive之后,不会直接调用这组销毁生命周期函数,而是在生命周期函数中新增两个:activateddeactivated

1.第一次进入页面时,生命周期函数执行简单来说是这样的: image.png 2.当退出时,会触发deactivated()函数;

3.当再次进入该页面时,只会执行activated()函数。

keep-alive用法总结

  • 在App.vue中使用keep-alive标签,表示缓存所有页面
 <div id="app">
  	<keep-alive>
	    <tar-bar></tar-bar>
	    <div class="container">
	      <left-menu></left-menu>
	      <Main />
	    </div>
    </keep-alive>
  </div>
  • 按条件缓存,使用include,exclude判断是否缓存
// 1. 将缓存 name 为 cc 的组件,如果有多个,可用逗号分
  	<keep-alive include='cc'>
      <router-view/>
    </keep-alive>
// 2. 将不缓存 name 为 cc 的组件
	<keep-alive exclude='cc'>
  	  <router-view/>
	</keep-alive>
// 3. 还可使用属性绑定动态判断
	<keep-alive :include='includedComs'>
  	  <router-view/>
	</keep-alive>
  • 在router目录下的index.js中
    • 使用meta:{keepAlive = true },表示需要缓存
    • 在App.vue中进行判断
 const routes = [
  {
    path:'/',
    component:Home,
    meta:{
        keepalive:true
    }
  },
  {
    path:'/login',
    component:Login
  },
   {
      path:'/edit',
      component:Edit,
      meta:{
          istoken: true
      }
   },
  {
      path:'/home',
      component:Home,
      meta:{
          keepalive:true
      }
  }
]