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 相关文章推荐
ExtJS4 表格的嵌套 rowExpander应用
May 02 Javascript
jQuery绑定事件不执行但alert后可以正常执行
Jun 03 Javascript
js事件监听机制(事件捕获)总结
Aug 08 Javascript
js实现点击图片改变页面背景图的方法
Feb 28 Javascript
JS实现iframe自适应高度的方法(兼容IE与FireFox)
Jun 24 Javascript
javascript实现table单元格点击展开隐藏效果(实例代码)
Apr 10 Javascript
JavaScript实现各种排序的代码详解
Aug 28 Javascript
写给小白看的JavaScript异步
Nov 29 Javascript
vue+iview写个弹框的示例代码
Dec 05 Javascript
解决使用bootstrap的dropdown部件时报错:error:Bootstrap dropdown require Popper.js问题
Aug 30 Javascript
VUE2.0+ElementUI2.0表格el-table循环动态列渲染的写法详解
Nov 30 Javascript
canvas 中如何实现物体的框选
Aug 05 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中函数rand和mt_rand的区别比较
2012/12/26 PHP
使用php-timeit估计php函数的执行时间
2015/09/06 PHP
分享PHP源码批量抓取远程网页图片并保存到本地的实现方法
2015/12/01 PHP
PHP+MySQL统计该库中每个表的记录数并按递减顺序排列的方法
2016/02/15 PHP
PHP+MySQL使用mysql_num_rows实现模糊查询图书信息功能
2018/05/31 PHP
零基础php编程好学吗
2019/10/11 PHP
JS 非图片动态loading效果实现代码
2010/04/09 Javascript
jQuery数据显示插件整合实现代码
2011/10/24 Javascript
将字符串中由空格隔开的每个单词首字母大写
2014/04/06 Javascript
jquery引用方法时传递参数原理分析
2014/10/13 Javascript
JavaScript实现大数的运算
2014/11/24 Javascript
在JavaScript中操作数组之map()方法的使用
2015/06/09 Javascript
使用AngularJS制作一个简单的RSS阅读器的教程
2015/06/18 Javascript
深入理解jQuery中的事件冒泡
2016/05/24 Javascript
ion content 滚动到底部会遮住一部分视图的快速解决方法
2016/09/06 Javascript
小程序云开发初探(小结)
2018/10/24 Javascript
JavaScript刷新页面的几种方法总结
2019/03/28 Javascript
通过javascript实现扫雷游戏代码实例
2020/02/09 Javascript
python编程实现12306的一个小爬虫实例
2017/12/27 Python
Python3之简单搭建自带服务器的实例讲解
2018/06/04 Python
Python socket非阻塞模块应用示例
2019/09/12 Python
3行Python代码实现图像照片抠图和换底色的方法
2019/10/10 Python
python scrapy重复执行实现代码详解
2019/12/28 Python
Django多数据库配置及逆向生成model教程
2020/03/28 Python
Python中格式化字符串的四种实现
2020/05/26 Python
终于搞懂了Keras中multiloss的对应关系介绍
2020/06/22 Python
html5开发三八女王节表白神器
2018/03/07 HTML / CSS
Sofmap官网:日本著名的数码电器专卖店
2017/05/19 全球购物
黄色火烈鸟:De Gele Flamingo
2019/03/18 全球购物
中国电子产品批发商/跨境电商/外贸网:Sunsky-online
2020/04/20 全球购物
大整数数相乘的问题
2012/07/22 面试题
敬业奉献模范事迹材料
2014/12/24 职场文书
和领导吃饭祝酒词
2015/08/11 职场文书
2019暑假阅读倡议书
2019/06/24 职场文书
为什么阅读对所有年龄段的孩子都很重要?
2019/07/08 职场文书
草系十大最强宝可梦,纸片人上榜,榜首大家最熟悉
2022/03/18 日漫