Vue中keep-alive原理

Posted 前端吕小布

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue中keep-alive原理相关的知识,希望对你有一定的参考价值。

定义

keep-alive是Vue中内置的一个抽象组件。它自身不会渲染一个 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 - 数字。最多可以缓存多少组件实例。

include 和 exclude 属性允许组件有条件地缓存:

<!-- 逗号分隔字符串 -->
<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实现缓存的核心代码就在这个钩子函数里。

  1. 先获取到插槽里的内容
  2. 调用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)

  1. 接下来会将这个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

  1. 缓存机制:接下来的事情很简单,根据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钩子

在这个钩子函数里,调用了pruneCache方法,以观测 include 和 exclude 的变化。

  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中 key keep-alive的实现原理

vue2.0提供了一个keep-alive组件用来缓存组件,避免多次加载相应的组件,减少性能消耗

keep-aliv是Vue.js的一个内置组件。它能够不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。

它有两个生命周期:

  1. activated: keep-alive组件激活时调用
  2. deactivated: keep-alive组件停用时调用

它提供了include与exclude两个属性,允许组件有条件地进行缓存。

keep-alive key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html>
<head>
  <title></title>
  <script type="text/javascript" src="./vue.js"></script>
</head>
<body>
  <div id="app">
    <keep-alive>
      <child-component key="1" v-if="seen" name="1"></child-component>
      <child-component key="2" v-if="!seen" name="2"></child-component>
    </keep-alive>
    <button @click="toggle">toggle</button>
  </div>
  <script type="text/javascript">
    Vue.component(‘child-component‘, {
      template: `<input type="text" placeholder="enter">`,
      data() {
        return {}
      },
      props: ["name"],
      mounted() {
        console.log(`${this.name} mounted`)
      }
    })
    const vm = new Vue({
      el: "#app",
      data: {
        seen: true
      },
      methods: {
        toggle() {
          this.seen = !this.seen;
        }
      }
    })
  </script>
</body>
</html>

key是标识元素不再被复用,注意key是Vue中的一个保留的属性,不能作为prop传递给子组件,否则会在控制台看到Vue的报错

但是keep-alive标识不重复创建组件实例,也就是只会触发一次created mounted事件,

利用两者可以对组件的复用进行比较精细的管理。

 

IOS数据存储技术开发视频教程 12课 附源码课件    下载地址:百度网盘下载

IOS界面开发UIKit详细使用视频教程 43课 附源码讲义    下载地址:百度网盘下载
IOS9零基础到精通大神班视频教程 包含源码课件面试题就业指导    下载地址:百度网盘下载
IOS培训班第四期 基础+就业班 完整高清版    下载地址:百度网盘下载
IOS培训班第六期 包含源码课件面试资料    下载地址:百度网盘下载
IOS培训班第五期【完整版】    下载地址:百度网盘下载
IOS多点触控与手势识别开发视频教程 10课 附源码    下载地址:百度网盘下载
中游学院Swift语言视频教程 教学视频 百度网盘下载    下载地址:百度网盘下载
极客ios开发工程师 全套视频教程 教学视频 百度网盘下载    下载地址:百度网盘下载
开源力量iOS开发从OC到Swift入门与修炼 视频教程 教学视频 百度网盘下载    下载地址:百度网盘下载
iOS常用UI控件使用速成秘籍 视频教程 教学视频 百度网盘下载    下载地址:百度网盘下载
IOS视频教程 全套编程自学基础入门app手机应用开发游戏项目实战    下载地址:百度网盘下载
IOS(iPhone)开发中级视频教程    下载地址:百度网盘下载
IOS开发实战视频教程    下载地址:百度网盘下载
小码哥大神班IOS五期不加密版本    下载地址:百度网盘下载
小马哥IOS9 苹果APP开发实战培训基础大神班视频教程    下载地址:百度网盘下载
黑马程序员iOS视频开发教程/李明杰    下载地址:百度网盘下载
IOS开发技术教程 极客学院最新IOS开发技术VIP教程入门到精通教程    下载地址:百度网盘下载
iOS中级视频教程    下载地址:百度网盘下载
iOS面试必看题    下载地址:百度网盘下载

 



















以上是关于Vue中keep-alive原理的主要内容,如果未能解决你的问题,请参考以下文章

vue.js 使用keep-alive设置返回不刷新

刷新 vue.js keep-alive 部分中的页面

vue.js+vue-router+webpack keep-alive用法

Vue.js前端框架系统学习(10)——动态组件 & 异步组件

Vue.js - 删除从 keep-alive 加载的子组件

vue.js 源代码学习笔记 ----- keep-alives