🐱 个人主页:不叫猫先生
🙋♂️ 作者简介:前端领域新星创作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀!
💫系列专栏:vue3从入门到精通、TypeScript从入门到实践
📢 资料领取:前端进阶资料以及文中源码可以找我免费领取
🔥 前端学习交流:博主建立了一个前端交流群,汇集了各路大神,一起交流学习,期待你的加入!(文末有我wx或者私信)
一、前世尘缘
vue中内置组件keep-alive的设计思想源于HTTP中的Keep-Alive模式,Keep-Alive模式避免频繁创建、销毁链接,允许多个请求和响应使用同一个HTTP链接。
HTTP 1.0 中keep-alive默认是关闭的,需要在HTTP头加入"Connection: Keep-Alive",才能启用Keep-Alive;HTTP 1.1中默认启用Keep-Alive,如果加入"Connection: close ",才关闭。目前大部分浏览器都是用HTTP 1.1协议。
二、keep-alive内置组件
作用:动态切换组件时缓存组件实例,避免dom重新渲染。
1.缓存动态组件
当组件为***ponentOne
时缓存该组件实例
<keep-alive :include="***ponentOne`" :exclude="***ponentTwo" :max="num">
<***ponent :is="current***ponent"></***ponent>
</keep-alive>
2.缓存路由组件
注意缓存路由组件vue2.x与vue3.x有区别,vue2.x用法如下:
<keep-alive :include="***ponentOne`" :exclude="***ponentTwo" :max="num">
<router-view :is="current***ponent"></router-view>
</keep-alive>
vue3.x用法如下:
<router-view v-slot="{ ***ponent }">
<keep-alive :include="includeList">
<***ponent :is="***ponent"/>
</keep-alive>
</router-view>
3.原理解析
缓存的组件以 [key,vnode] 的形式记录,keys记录缓存的组件key,依据inclued、exclude的值,并且当超过设置的max根据LUR算法进行清除。vue2.x和vue3.x相差不大。
(1)keep-alive 在生命周期中做了什么?
- created:初始化catch,keys。catch是一个缓存组件虚拟dom的数组,其中数组中对象的key是组件的key,value是组件的虚拟dom;keys是一个用来缓存组件的key的数组。
- mounted:实时监听include、exclude属性的变化,并执行相应操作。
- destroyed:删除掉所有缓存相关的数据。
(2)源码
地址:源码地址
// 源码位置:src/core/***ponents/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 () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
//查看是否有缓存没有缓存的话直接走缓存
this.cacheVNode()
// 这里借助 watch 监控 include 和 exclude
// 如果有变化的话,则按照最新的 include 和 exclude 更新 this.cache
// 将不满足 include、exclude 限制的 缓存vnode 从 this.cache 中移除
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated() {
this.cacheVNode()
},
methods:{
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, ***ponentInstance, ***ponentOptions } = vnodeToCache
cache[keyToCache] = {
name: _get***ponentName(***ponentOptions),
tag,
***ponentInstance
}
keys.push(keyToCache)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
render(){
//下面详细介绍
}
}
(3)abstract:true
设置为true时,表面该组件为抽象组件,抽象组件不会和子组件建立父子关系,组件实例会根据这个属性决定是否忽略该组件,所以并不会有节点渲染在页面中。
(4)pruneCacheEntry函数
destoryed周期中循环了所有缓存的组件,并用 pruneCacheEntry
进行处理,pruneCacheEntry做了什么事?
// src/core/***ponents/keep-alive.js
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.***ponentInstance.$destroy() // 执行组件的destory钩子函数
}
cache[key] = null // cache中对象的key设为null
remove(keys, key) // 删除keys对应的元素
}
destoryed周期中,删除缓存组件的所有数组,pruneCacheEntry
主要做了这几件事:
- 遍历缓存组件集合(cach),对所有缓存的组件执行$destroy方法
- 清除cache中key的值
- 清除keys中的key
(5)render
render中主要做了什么?
- 获取keep-alive组件子节点中第一个组件的vnode、***ponentOptions、name
- 如果name存在且不在include中或者存在在exclude中,则返回虚拟dom。此时该组件并没有使用缓存。
- 接下来就是上面的else情况:使用
keep-alive
进行组件缓存,根据组件id,tag生成组件的key,如果cache集合中存在以key为属性名的vdom,,说明组件已经缓存过,则将缓存的 Vue 实例赋值给vnode.***ponentInstance
,从keys中删除key,再把key push导keys中,保证当前key在keys的最后面(这是LRU算法的关键)。如果不存在则继续走下面 - 如果cach[key]不存在则为第一次加载组件,则把vdom赋值给cach[key],key push到key
- 如果keys的长度大于max,则进行组件缓存清理,则把不经常使用的被缓存下来的在keys中排第一位的组件清除掉,清除也是调用的pruneCacheEntry方法
render () {
// 获取 keep-alive 组件子节点中的第一个组件 vnode
const slot = this.$slots.default
const vnode = getFirst***ponentChild(slot)
// 获取组件的配置选项对象
const ***ponentOptions = vnode && vnode.***ponentOptions
if (***ponentOptions) {
// 获取组件的名称
const name = _get***ponentName(***ponentOptions)
const { include, exclude } = this
// 如果当前的组件 name 不在 include 中或者组件的 name 在 exclude 中
// 说明当前的组件是不被 keep-alive 所缓存的,此时直接 return vnode 即可
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// 代码执行到这里,说明当前的组件受 keep-alive 组件的缓存
const { cache, keys } = this
// 定义 vnode 缓存用的 key
const key =
vnode.key == null
? // same constructor may get registered as different local ***ponents
// so cid alone is not enough (#3269)
***ponentOptions.Ctor.cid +
(***ponentOptions.tag ? `::${***ponentOptions.tag}` : '')
: vnode.key
// 如果 cache[key] 已经存在的话,则说明当前的组件 vnode 已经被缓存过了,此时需要将其恢复还原出来
if (cache[key]) {
// 将缓存的 Vue 实例赋值给 vnode.***ponentInstance
vnode.***ponentInstance = cache[key].***ponentInstance
// make current key freshest
// 先从 keys 中移除 key,然后再 push key,这可以保证当前的 key 在 keys 数组中的最后面
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
// 如果 cache[key] 不存在的话,说明当前的子组件是第一次出现,此时需要将 vnode 缓存到 cache 中,将 key 存储到 keys 字符串数组中。这里是用一个中间变量接收,当数据变化时触发updated去调用cacheVNode方法。
this.vnodeToCache = vnode
this.keyToCache = key
}
// @ts-expect-error can vnode.data can be undefined
// 将 vnode.data.keepAlive 属性设置为 true,这对 vnode 有一个标识的作用,标识这个
// vnode 是 keep-alive 组件的 render 函数 return 出去的,这个标识在下面的运行代码中有用
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
三、LRU算法
缓存的组件在进行清除的时候使用了LRU算法,具体是什么策略呢?当数据超过了限定空间的时候对数据清理,清理的原则是对很久没有使用到过的数据进行清除。