keep-alive保持组件状态的方法


Posted in Javascript onDecember 02, 2020

keep-alive的设计初衷

有些业务场景需要根据不同的判断条件,动态地在多个组件之间切换。频繁的组件切换会导致组件反复渲染,如果组件包含有大量的逻辑和dom节点,极易造成性能问题。其次,切换后组件的状态也会完全丢失。keep-alive的设计初衷就是为了保持组件的状态,避免组件的重复渲染。

为什么keep-alive可以直接使用

开发者无需注册和引入,直接可以在模板中使用。 跟开发者使用Vue.component自定义的组件不同,keep-alive无需注册,在模板中直接可以使用,如下所示:

<keep-alive>
 <component :is="view"></component>
</keep-alive>

这是因为keep-alive是vue的内置组件,已经在vue中提前定义。

// core/components/keep-alive.js

export default {
 name: 'keep-alive',
 abstract: true,

 props: {
  include: patternTypes,
  exclude: patternTypes,
  max: [String, Number]
 },

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

 destroyed () {
  // keep-alive的销毁,将所有缓存的组件清除
  for (const key in this.cache) {
   pruneCacheEntry(this.cache, key, this.keys)
  }
 },

 mounted () {
  // 如果指定了include和exclude属性,需要实时观察当前这两个属性的变化,以及时的更新缓存
  this.$watch('include', val => {
   pruneCache(this, name => matches(val, name))
  })
  this.$watch('exclude', val => {
   pruneCache(this, name => !matches(val, name))
  })
 },

 render () {
  // keepAlive组件本身不会被渲染成dom节点,其render方法的处理逻辑的是将其包裹的组件的vnode返回
  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

   // 1、如果缓存中存在该vnode,从缓存中取得该组件的实例(一个组件对应一颗vnode树,同时一个组件对应一个vue子类的实例),不再重新创建
   if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    // make current key freshest
    // 将当前的组件的key作为最新的缓存(更新其在keys数组中的顺序)
    remove(keys, key)
    keys.push(key)
   } else {
    // 2、如果未命中缓存,添加到缓存
    cache[key] = vnode
    keys.push(key)
    // 如果缓存超过限制,淘汰最旧的缓存
    if (this.max && keys.length > parseInt(this.max)) {
     pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
   }

   // 标记为keepAlive组件
   vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])
 }
}

这是因为keep-alive是vue的内置组件,已经在vue中提前定义。

// core/components/keep-alive.js

export default {
 name: 'keep-alive',
 abstract: true,

 props: {
  include: patternTypes,
  exclude: patternTypes,
  max: [String, Number]
 },

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

 destroyed () {
  // keep-alive的销毁,将所有缓存的组件清除
  for (const key in this.cache) {
   pruneCacheEntry(this.cache, key, this.keys)
  }
 },

 mounted () {
  // 如果指定了include和exclude属性,需要实时观察当前这两个属性的变化,以及时的更新缓存
  this.$watch('include', val => {
   pruneCache(this, name => matches(val, name))
  })
  this.$watch('exclude', val => {
   pruneCache(this, name => !matches(val, name))
  })
 },

 render () {
  // keepAlive组件本身不会被渲染成dom节点,其render方法的处理逻辑的是将其包裹的组件的vnode返回
  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

   // 1、如果缓存中存在该vnode,从缓存中取得该组件的实例(一个组件对应一颗vnode树,同时一个组件对应一个vue子类的实例),不再重新创建
   if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    // make current key freshest
    // 将当前的组件的key作为最新的缓存(更新其在keys数组中的顺序)
    remove(keys, key)
    keys.push(key)
   } else {
    // 2、如果未命中缓存,添加到缓存
    cache[key] = vnode
    keys.push(key)
    // 如果缓存超过限制,淘汰最旧的缓存
    if (this.max && keys.length > parseInt(this.max)) {
     pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
   }

   // 标记为keepAlive组件
   vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])
 }
}
// core/components/index.js
import KeepAlive from './keep-alive'

export default {
 KeepAlive
}
// core/global-api/index.js

// 导入内置组件
import builtInComponents from '../components/index'

/**
 * 为Vue添加全局方法和属性
 * @param {GlobalAPI} Vue 
 */
export function initGlobalAPI (Vue: GlobalAPI) {
 
 // ...省略了无关代码
 
 Vue.options = Object.create(null)
 // 添加内置组件keep-alive
 extend(Vue.options.components, builtInComponents)
}

buildInComponents中包含了keep-alive的定义。在initGlobalAPI方法中,将内置组件添加到了 vue的全局变量中。

extend(A, B)是个简单的对象属性复制方法。将对象B中的属性复制到对象A中。

keep-alive是如何保持组件状态的

为了保持组件状态,keep-alive设计了缓存机制。

我们知道,模板中的每个HTML标签在vue中由相应的vnode节点对象来表示。如果是HTML标签是组件标签,需要为该标签的vnode创建一个组件实例。组件实例负责组件内的HTML模板的编译和渲染。因此相比于普通HTML标签的vnode节点,组件vnode节点会存在componentOptions和 componentInstance 这两个属性中保存组件选项对象和组件实例的引用。

首先,我们从keep-alive组件的实现代码中可以看到在组件的created钩子中设计了缓存机制:

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

keep-alive设置了cache和keys两个属性来缓存子组件。其中cache中的每项是一个以所包裹的组件的组件名为key,包裹组件对应的vnoded为值的对象。keys的每一项是其所包裹的组件的组件名。

render 函数是vue实例和vue组件实例中用来创建vnode的方法。我们在实际的应用中,一般是通过template和el来指定模板,然后由vue将模板编译成render函数。如果用户希望能更灵活地控制vnode的创建可以提供自定义的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

   // 1、如果缓存中存在该vnode,从缓存中取得该组件的实例(一个组件对应一颗vnode树,同时一个组件对应一个vue子类的实例),不再重新创建
   if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    // make current key freshest
    // 将当前的组件的key作为最新的缓存(更新其在keys数组中的顺序)
    remove(keys, key)
    keys.push(key)
   } else {
    // 2、如果未命中缓存,添加到缓存
    cache[key] = vnode
    keys.push(key)
    // 如果缓存超过限制,淘汰最旧的缓存
    if (this.max && keys.length > parseInt(this.max)) {
     pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
   }

   // 标记为keepAlive组件
   vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])
 }

keep-alive内部就是单独提供了render函数来自定义了vnode的创建逻辑。首先keep-alive获取到其所包裹的子组件的根vnode,然后去cache中查找该组件是否存在。

如果cache中不存在子组件vnode,则以{子组件名: 子组件vnode}的形式保存到cache对象中。同时将子组件名字保存到keys数组中。同时如果当前缓存的数量已经超过max所设置的最大值,需要淘汰掉最近最少使用的缓存项(LRU)。

如果cache中存在子组件vnode,那么只需要复用缓存的组件vnode的组件实例(componentInstance)。同时需要将该子组件vnode在缓存中顺序调到最前面,这个主要是为了在缓存不足时,正确地淘汰缓存项。

举例说明

最后通过一个例子加深一下理解。

<div id="app">
  <keep-alive><component :is="view"></component></keep-alive>
  <button @click="view = view =='count'? 'any': 'count'">切换组件</button>
</div>
Vue.component("count", {
  data() {
    return {
      count:0
    };
  },
  template: "<div @click='count+=1'>点了我 {{count}} 次</div>"
});
  
Vue.component("any", {
  template: "<div>any</div>"
});

new Vue({
  el: "#app",
  data: {
   view: "count"
  }
});

由于view默认值是count,因此keep-alive包裹的子组件是count。此时keep-alive的缓存中为空,因此会把组件count的vnode添加到缓存。缓存结果为

cache = {1::count: {tag: "vue-component-1-count", data:{tag: "component", hook: {…}}}, componentOptions, componentInstance, ...}
keys = ["1::count"]

keep-alive保持组件状态的方法

点击一下组件count,组件的显示内容变成"点了我1次",然后切换到组件any。与count组件相同,由于在keep-alive的缓存中还未保存any组件的vnode,因此需要将any添加到缓存中。此时缓存结果变成了:

cache = {
  1::count: {tag: "vue-component-1-count", data:{tag: "component", hook: {…}}, componentOptions, componentInstance, ...},
  2::any: {tag: "vue-component-2-any", data:{tag: "component", hook: {…}}, componentOptions, componentInstance, ...},
}
keys = ["1::count", "2::any"]

页面显示结果为:

keep-alive保持组件状态的方法

再次点击切换组件,切回count。此时count组件的vnode在缓存中已经存在,所以直接复用了原来count组件vnode中所保存的组件实例,组件实例中保存了原来的count值,因此组件切换完后,组件的状态也跟着还原了回来。

下图为count组件实例的状态,可以看到count的值得到了保持:

keep-alive保持组件状态的方法

最终页面显示结果为:

keep-alive保持组件状态的方法

从上面的分析可知,如果组件被包裹在keep-alive组件中,组件vnode会缓存到cache中。而组件的状态又会保存在组件实例中(componentInstance),当组件再次切换回来时,keep-alive直接将之前缓存的状态进行还原,也就实现了组件状态的保持。

以上就是keep-alive保持组件状态的方法的详细内容,更多关于keep-alive保持组件状态的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
获取元素距离浏览器周边的位置的方法getBoundingClientRect
Apr 17 Javascript
js的flv视频播放器插件使用方法
Jun 23 Javascript
javascript实现Email邮件显示与删除功能
Nov 21 Javascript
vue-router:嵌套路由的使用方法
Feb 21 Javascript
vue.js实例todoList项目
Jul 07 Javascript
BootStrap的双日历时间控件使用
Jul 25 Javascript
axios的拦截请求与响应方法
Aug 11 Javascript
解决vue点击控制单个样式的问题
Sep 05 Javascript
微信小程序实现通过js操作wxml的wxss属性示例
Dec 06 Javascript
关于layui 下拉列表的change事件详解
Sep 20 Javascript
vue addRoutes路由动态加载操作
Aug 04 Javascript
详解vue修改elementUI的分页组件视图没更新问题
Nov 13 Javascript
vue组件中节流函数的失效的原因和解决方法
Dec 02 #Vue.js
Vue3+elementui plus创建项目的方法
Dec 01 #Vue.js
JS实现超级好看的鼠标小尾巴特效
Dec 01 #Javascript
echarts饼图各个板块之间的空隙如何实现
Dec 01 #Javascript
浅谈es6中的元编程
Dec 01 #Javascript
Vue.js桌面端自定义滚动条组件之美化滚动条VScroll
Dec 01 #Vue.js
微信小程序轮播图swiper代码详解
Dec 01 #Javascript
You might like
PHP系统流量分析的程序
2006/10/09 PHP
php查找任何页面上的所有链接的方法
2013/12/03 PHP
利用PHP将图片转换成base64编码的实现方法
2016/09/13 PHP
AJAX的使用方法详解
2017/04/29 PHP
PHP简单实现遍历目录下特定文件的方法小结
2017/05/22 PHP
Ajax+PHP实现的模拟进度条功能示例
2019/02/11 PHP
brook javascript框架介绍
2011/10/10 Javascript
JS合并数组的几种方法及优劣比较
2014/09/19 Javascript
JQuery插件Marquee.js实现无缝滚动效果
2016/04/26 Javascript
动态JavaScript所造成一些你不知道的危害
2016/09/25 Javascript
node.js爬虫爬取拉勾网职位信息
2017/03/14 Javascript
Javascript 实现匿名递归的实例代码
2017/05/25 Javascript
详解Node.js access_token的获取、存储及更新
2017/06/20 Javascript
Angular6 写一个简单的Select组件示例
2018/08/20 Javascript
Vue使用lodop实现打印小结
2019/07/06 Javascript
解决LayUI加上form.render()下拉框和单选以及复选框不出来的问题
2019/09/27 Javascript
layui写后台表格思路和赋值用法详解
2019/11/14 Javascript
JS实现省市县三级下拉联动
2020/04/10 Javascript
Vue自定义render统一项目组弹框功能
2020/06/07 Javascript
[04:27]DOTA2官方论坛水友赛集锦
2013/09/16 DOTA
python2.7删除文件夹和删除文件代码实例
2013/12/18 Python
Python连接MySQL并使用fetchall()方法过滤特殊字符
2016/03/13 Python
python通过socket实现多个连接并实现ssh功能详解
2017/11/08 Python
Python3.6日志Logging模块简单用法示例
2018/06/14 Python
Python人脸识别第三方库face_recognition接口说明文档
2019/05/03 Python
Django认证系统实现的web页面实现代码
2019/08/12 Python
详解css3中的伪类before和after常见用法
2020/11/17 HTML / CSS
Silk’n激光脱毛器官网:silkn.com
2016/10/06 全球购物
文员自我评价怎么写
2013/09/19 职场文书
中英文求职信范文
2014/01/27 职场文书
学生感冒英文请假条
2014/02/04 职场文书
多媒体编辑专业毕业生求职信
2014/06/13 职场文书
火烧圆明园的观后感
2015/06/03 职场文书
信息简报范文
2015/07/21 职场文书
Nginx反向代理多个服务器的实现方法
2021/03/31 Servers
详解运行Python的神器Jupyter Notebook
2021/06/03 Python