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 相关文章推荐
jquery focus(fn),blur(fn)方法实例代码
Dec 16 Javascript
js动态添加onload、onresize、onscroll事件(另类方法)
Dec 26 Javascript
理解jQuery stop()方法
Nov 21 Javascript
详解JavaScript中数组的相关知识
Jul 29 Javascript
分享JavaScript监听全部Ajax请求事件的方法
Aug 28 Javascript
[Bootstrap-插件使用]Jcrop+fileinput组合实现头像上传功能实例代码
Dec 20 Javascript
微信小程序 PHP生成带参数二维码
Feb 21 Javascript
详解webpack + vue + node 打造单页面(入门篇)
Sep 23 Javascript
浅谈vue项目可以从哪些方面进行优化
May 05 Javascript
Vue slot用法(小结)
Oct 22 Javascript
vue集成openlayers加载geojson并实现点击弹窗教程
Sep 24 Javascript
vue中activated的用法
Jan 03 Vue.js
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
什么是短波收听SWL
2021/03/01 无线电
Smarty模板快速入门
2007/01/04 PHP
php 执行系统命令的方法
2009/07/07 PHP
浅析php fwrite写入txt文件的时候用 \r\n不能换行的问题
2013/08/06 PHP
js 数组克隆方法 小结
2010/03/20 Javascript
浅谈javascript中自定义模版
2015/01/29 Javascript
浅谈Javascript的静态属性和原型属性
2015/05/07 Javascript
JavaScript判断表单中多选框checkbox选中个数的方法
2015/08/17 Javascript
跟我学习javascript的基本类型和引用类型
2015/11/16 Javascript
jquery trigger函数执行两次的解决方法
2016/02/29 Javascript
confirm确认对话框的实现方法总结
2016/06/17 Javascript
vue2.0父子组件间通信的实现方法
2017/04/19 Javascript
React Native日期时间选择组件的示例代码
2018/04/27 Javascript
详解如何在webpack中做预渲染降低首屏空白时间
2018/08/22 Javascript
react build 后打包发布总结
2018/08/24 Javascript
深入探讨JavaScript的最基本部分之执行上下文
2019/02/12 Javascript
Vue 请求传公共参数的操作
2020/07/31 Javascript
vue项目接口管理,所有接口都在apis文件夹中统一管理操作
2020/08/13 Javascript
python处理中文编码和判断编码示例
2014/02/26 Python
Python单元测试框架unittest简明使用实例
2015/04/13 Python
Python实现的寻找前5个默尼森数算法示例
2018/03/25 Python
PyTorch快速搭建神经网络及其保存提取方法详解
2018/04/28 Python
python排序函数sort()与sorted()的区别
2018/09/18 Python
python读取文本中的坐标方法
2018/10/14 Python
网易有道2017内推编程题 洗牌(python)
2019/06/19 Python
python的数学算法函数及公式用法
2020/11/18 Python
CSS3教程(5):网页背景图片
2009/04/02 HTML / CSS
AE美国鹰日本官方网站: American Eagle Outfitters
2016/12/10 全球购物
加热夹克:RAVEAN
2018/10/19 全球购物
世界上最受欢迎的花店:1-800-Flowers.com
2020/06/01 全球购物
激情洋溢的毕业生就业求职信
2014/03/15 职场文书
2014年五四青年节活动方案
2014/03/29 职场文书
关于安全的广播稿
2014/10/23 职场文书
员工离职感谢信
2015/01/22 职场文书
2016公司中秋节寄语
2015/12/07 职场文书
多线程Spring通过@Scheduled实现定时任务
2022/05/25 Java/Android