从源码角度来回答keep-alive组件的缓存原理


Posted in Javascript onJanuary 18, 2021

今天开门见山地聊一下面试中被问到的一个问题:keep-alive组件的缓存原理。

官方API介绍和用法

  • <keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
  • 和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
  • 当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。

官网的例子是 tab 切换保存了用户的操作,实际中还可能遇到从列表页跳转去了详情页,再跳转回列表页需要保存用户进行过的筛选操作,这就需要用到 <keep-alive>,这样也能避免重新渲染,提高页面性能。

用法及props的讲解

// keep-alive组件搭配动态组件的用法,还要其他的用法参见官网
<keep-alive
 include="['componentNameA', 'componentNameB']"
 exclude="'componentNameC'"
 :max="10">
 <component :is="view"></component>
</keep-alive>
  • include - 字符串或正则表达式或数组,name匹配上的组件会被缓存
  • exclude - 字符串或正则表达式或数组,name匹配上的组件都不会被缓存
  • max - 字符串或数字,缓存组件实例的最大数,最久没有被访问的实例会被销毁掉

注意:

  • <keep-alive> 只渲染其直系的一个组件,因此若在 <keep-alive> 中用 v-for,则其不会工作,若多条件判断有多个符合条件也同理不工作。
  • include 和 exclude 匹配时,首先检查组件的 name 选项,若 name 选项不可用,则匹配它的局部注册名称 (即父组件 components 选项的键值)。匿名组件不能被匹配。
  • <keep-alive> 不会在函数式组件中正常工作,因为它们没有缓存实例。

源码解读

先贴一张源码图

从源码角度来回答keep-alive组件的缓存原理

总共125行,收起来一看其实东西也比较少。前面是引入一些需要用到的方法,然后定义了一些  keep-alive 组件自己会用到的一些方法,最后就是向外暴露一个 name 为 keep-alive 的组件选项,这些选项除了 abstract 外,其他的我们都比较熟悉了,其中, render 函数就是缓存原理最重要的部分,也能看出 keep-alive 组件是一个函数式组件。

// isRegExp函数判断是不是正则表达式,remove移除数组中的某一个成员
// getFirstComponentChild获取VNode数组的第一个有效组件
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
​
type VNodeCache = { [key: string]: ?VNode }; // 缓存组件VNode的缓存类型
​
// 通过组件的name或组件tag来获取组件名(上面注意的第二点)
function getComponentName (opts: ?VNodeComponentOptions): ?string {
 return opts && (opts.Ctor.options.name || opts.tag)
}
​
// 判断include或exclude跟组件的name是否匹配成功
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
 if (Array.isArray(pattern)) {
 return pattern.indexOf(name) > -1 // include或exclude是数组的情况
 } else if (typeof pattern === 'string') {
 return pattern.split(',').indexOf(name) > -1 // include或exclude是字符串的情况
 } else if (isRegExp(pattern)) {
 return pattern.test(name) // include或exclude是正则表达式的情况
 }
 return false // 都没匹配上(上面注意的二三点)
}
​
// 销毁缓存
function pruneCache (keepAliveInstance: any, filter: Function) {
 const { cache, keys, _vnode } = keepAliveInstance // keep-alive组件实例
 for (const key in cache) {
 const cachedNode: ?VNode = cache[key] // 已经被缓存的组件
 if (cachedNode) {
  const name: ?string = getComponentName(cachedNode.componentOptions)
  // 若name存在且不能跟include或exclude匹配上就销毁这个已经缓存的组件
  if (name && !filter(name)) {
  pruneCacheEntry(cache, key, keys, _vnode)
  }
 }
 }
}
​
// 销毁缓存的入口
function pruneCacheEntry (
 cache: VNodeCache,
 key: string,
 keys: Array<string>,
 current?: VNode
) {
 const cached = cache[key] // 被缓存过的组件
 // “已经被缓存的组件是否继续被缓存” 有变动时
 // 若组件被缓存命中过且当前组件不存在或缓存命中组件的tag和当前组件的tag不相等
 if (cached && (!current || cached.tag !== current.tag)) {
 // 说明现在这个组件不需要被继续缓存,销毁这个组件实例
 cached.componentInstance.$destroy()
 }
 cache[key] = null // 把缓存中这个组件置为null
 remove(keys, key) // 把这个组件的key移除出keys数组
}
​
// 示例类型
const patternTypes: Array<Function> = [String, RegExp, Array]
​
// 向外暴露keep-alive组件的一些选项
export default {
 name: 'keep-alive', // 组件名
 abstract: true, // keep-alive是抽象组件
​
 // 用keep-alive组件时传入的三个props
 props: {
 include: patternTypes,
 exclude: patternTypes,
 max: [String, Number]
 },
​
 created () {
 this.cache = Object.create(null) // 存储需要缓存的组件
 this.keys = [] // 存储每个需要缓存的组件的key,即对应this.cache对象中的键值
 },
​
 // 销毁keep-alive组件的时候,对缓存中的每个组件执行销毁
 destroyed () {
 for (const key in this.cache) {
  pruneCacheEntry(this.cache, key, this.keys)
 }
 },
​
 // keep-alive组件挂载时监听include和exclude的变化,条件满足时就销毁已缓存的组件
 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 // keep-alive组件的默认插槽
 const vnode: VNode = getFirstComponentChild(slot) // 获取默认插槽的第一个有效组件
 // 如果vnode存在就取vnode的选项
 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
 if (componentOptions) {
  //获取第一个有效组件的name
  const name: ?string = getComponentName(componentOptions)
  const { include, exclude } = this // props传递来的include和exclude
  if (
  // 若include存在且name不存在或name未匹配上
  (include && (!name || !matches(include, name))) ||
  // 若exclude存在且name存在或name匹配上
  (exclude && name && matches(exclude, name))
  ) {
  return vnode // 说明不用缓存,直接返回这个组件进行渲染
  }
  
  // 匹配上就需要进行缓存操作
  const { cache, keys } = this // keep-alive组件的缓存组件和缓存组件对应的key
  // 获取第一个有效组件的key
  const key: ?string = vnode.key == null
  // 同一个构造函数可以注册为不同的本地组件
  // 所以仅靠cid是不够的,进行拼接一下
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  : vnode.key
  // 如果这个组件命中缓存
  if (cache[key]) {
  // 这个组件的实例用缓存中的组件实例替换
  vnode.componentInstance = cache[key].componentInstance
  // 更新当前key在keys中的位置
  remove(keys, key) // 把当前key从keys中移除
  keys.push(key) // 再放到keys的末尾
  } else {
  // 如果没有命中缓存,就把这个组件加入缓存中
  cache[key] = vnode
  keys.push(key) // 把这个组件的key放到keys的末尾
  // 如果缓存中的组件个数超过传入的max,销毁缓存中的LRU组件
  if (this.max && keys.length > parseInt(this.max)) {
   pruneCacheEntry(cache, keys[0], keys, this._vnode)
  }
  }
​
  vnode.data.keepAlive = true // 设置这个组件的keepAlive属性为true
 }
 // 若第一个有效的组件存在,但其componentOptions不存在,就返回这个组件进行渲染
 // 或若也不存在有效的第一个组件,但keep-alive组件的默认插槽存在,就返回默认插槽的第一个组件进行渲染
 return vnode || (slot && slot[0])
 }
}

补充:

上面关于删除第一个旧缓存组件和更新缓存组件 key 的顺序,其实是用到了LRU缓存淘汰策略:
LRU全称Least Recently Used,最近最少使用的意思,是一种内存管理算法。
这种算法基于一种假设:长期不用的数据,在未来被用到的几率也很小,因此,当数据所占内存达到一定阈值,可以移除掉最近最少使用的。

总结

简单总结为:

keep-alive 组件在渲染的时候,会根据传入的 include 和 exclude 来匹配 keep-alive 包裹的命名组件,未匹配上就直接返回这个命名组件进行渲染,若匹配上就进行缓存操作:若缓存中已有这个组件,就替换其实例,并更新这个组件的 key 在 keys 中的位置;若缓存中没有这个组件,就把这个组件放入 keep-alive 组件的缓存 cache 中,并把这个组件的 key 放入 keys 中,由于在 mounted 的时候有对 include 和 exclude 进行监听,因此,后续这两个属性值发生变化时,会再次判断是否满足条件而进行组件销毁。

到此这篇关于从源码角度来回答keep-alive组件的缓存原理的文章就介绍到这了,更多相关keep-alive组件缓存内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
jQuery生成asp.net服务器控件的代码
Feb 04 Javascript
javascript是怎么继承的介绍
Jan 05 Javascript
js动态往表格的td中添加图片并注册事件
Jun 12 Javascript
web前端开发JQuery常用实例代码片段(50个)
Aug 28 Javascript
js点击文本框弹出可选择的checkbox复选框
Feb 03 Javascript
使用jQuery实现Web页面换肤功能的要点解析
May 12 Javascript
为什么JavaScript没有块级作用域
May 22 Javascript
基于JavaScript实现右键菜单和拖拽功能
Nov 28 Javascript
jQuery基于ajax操作json数据简单示例
Jan 05 Javascript
vue实例中data使用return包裹的方法
Aug 27 Javascript
angular将html代码输出为内容的实例
Sep 30 Javascript
javascript开发实现贪吃蛇游戏
Jul 31 Javascript
在JavaScript中查找字符串中最长单词的三种方法(推荐)
Jan 18 #Javascript
vue-resource 拦截器interceptors使用详解
Jan 18 #Vue.js
js数组的基本使用总结
Jan 18 #Javascript
JavaScript/TypeScript 实现并发请求控制的示例代码
Jan 18 #Javascript
js加减乘除精确运算方法实例代码
Jan 17 #Javascript
Angular处理未可知异常错误的方法详解
Jan 17 #Javascript
react-native 实现购物车滑动删除效果的示例代码
Jan 15 #Javascript
You might like
Apache中php.ini的设置方法
2013/02/28 PHP
php输出xml必须header的解决方法
2014/10/17 PHP
PHP两种实现无级递归分类的方法
2017/03/02 PHP
php使用ftp实现文件上传与下载功能
2017/07/21 PHP
javascript cookies操作集合
2010/04/12 Javascript
Javascript前端UI框架Kit使用指南之kitjs事件管理
2014/11/28 Javascript
JQuery实现网页右侧随动广告特效
2016/01/17 Javascript
BootStrap和jQuery相结合实现可编辑表格
2016/04/21 Javascript
EasyUI的doCellTip实现鼠标放到单元格上提示单元格内容
2016/08/24 Javascript
浅谈jquery页面初始化的4种方式
2016/11/27 Javascript
Javascript中关于Array.filter()的妙用详解
2016/12/04 Javascript
Node.js连接mongodb实例代码
2017/06/06 Javascript
Angular4 中内置指令的基本用法
2017/07/31 Javascript
js原生日历的实例(推荐)
2017/10/31 Javascript
基于jQuery的$.getScript方法去加载javaScript文档解析
2017/11/08 jQuery
Vue.js与 ASP.NET Core 服务端渲染功能整合
2017/11/16 Javascript
js中getBoundingClientRect的作用及兼容方案详解
2018/02/01 Javascript
公众号SVG动画交互实战代码
2020/05/31 Javascript
python实现堆栈与队列的方法
2015/01/15 Python
Python使用爬虫猜密码
2016/02/19 Python
简述Python中的进程、线程、协程
2016/03/18 Python
PyQt5下拉式复选框QComboCheckBox的实例
2019/06/25 Python
Python 爬虫实现增加播客访问量的方法实现
2019/10/31 Python
Python如何使用内置库matplotlib绘制折线图
2020/02/24 Python
Python多线程:主线程等待所有子线程结束代码
2020/04/25 Python
Perfume’s Club法国站:购买香水和化妆品
2019/05/02 全球购物
泰国国际航空公司官网:Thai Airways International
2019/12/04 全球购物
总经理岗位职责
2013/11/09 职场文书
求职信名称怎么写
2014/05/26 职场文书
医学求职信
2014/05/28 职场文书
学生安全责任书模板
2014/07/25 职场文书
大学生职业生涯规划大赛作品(精品)
2014/09/17 职场文书
流动人口婚育证明范本
2014/09/26 职场文书
运动会广播稿200字
2014/10/18 职场文书
《思路决定出路》读后感3篇
2019/12/11 职场文书
一篇带你入门Java垃圾回收器
2021/06/16 Java/Android