结论先行:
<keep-alive>是 Vue 的内置组件,主要是用于缓存组件的实例,避免组件重复的被创建和销毁组件,提高应用的响应速度和性能。
原理:keep-alive 是一个缓存,会标记这个虚拟节点被缓存过了,后续就不会重新初始化,也不会进行销毁。
那常见的配置属性有 include 、exclude 和 max;
include 和 exclude 用于指定需要缓存或排除的组件名称;
而 max 是用来定义组件的最大缓存个数。
内部采用 LRU 算法用于维护缓存列表。如果缓存个数超过最大数,那么会将最久没有被访问到的组件移出缓存列表,也就是销毁组件。
keep-alive 提供了两个钩子函数: activated 钩子和 deactivated 钩子。
activated:当组件被激活(使用)的时候触发,常用于执行一些数据初始化或者异步操作;
deactivated:当组件失活(不被使用)的时候触发;
但是在使用 keep-alive 时,要注意数据的更新问题,避免出现数据无法更新的情况。
一、背景
在平常开发中,有部分组件没有必要多次初始化。这时,我们需要将组件进行持久化,使组件的状态维持不变。在下一次展示时,也不会进行重新初始化组件。keep-alive就有这个功能。
二、含义
<keep-alive>是 Vue 的内置组件,可以使被包裹的组件保留状态,避免重复的创建和销毁组件,提高应用的响应速度和性能。
当一个组件被<keep-alive>标签包裹时,会缓存组件的实例在内存中,而不会把组件销毁。当这个组件再次被使用时,Vue会从缓存中提取组件的实例,将其重新挂载到页面上。
这个功能可以提高应用的性能,特别是在需要频繁切换组件的场景下,就比如Tab切换或者路由切换,因为不需要每一次切换时都重新创建和销毁组件,而是直接从缓存中获取,这样可以避免重复的初始化和渲染,从而提高应用的响应速度和性能。
举个应用场景:
有个员工列表,现在我们点击某条数据,查看员工详情后,再返回到员工列表。这个时候我们就希望这个列表能够保持刚才的状态,这时候就可以使用keep-alive把这个列表所在的组件包裹。
和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
三、配置属性
include 和 exclude用于指定需要缓存或排除的组件名称;
max 和 min 用于指定缓存的最大和最小数量。
① include:包含
值为字符串 / 正则表达式 / 数组。只有组件的名称(name)与 include 的值相同的才会被缓存。即指定哪些被缓存,可以指定多个被缓存。
这里以字符串为例,指定多个组件缓存,语法是用逗号隔开。如下:
// 指定 home 组件和 about 组件被缓存
<keep-alive include="home,about">
<router-view></router-view>
</keep-alive>
② exclude:排除,优先级大于 include
同上,指定哪些组件不被缓存。
// 除了home组件和about组件,别的都缓存
<keep-alive exclude="home,about">
<router-view></router-view>
</keep-alive>
使用 include 和 exclude 属性时,缓存组件的名称 name 一定要赋值,否则无法识别到对应的路由组件。
如果
name
选项不可用,则匹配它的局部注册名称 (父组件components
选项的键值),匿名组件不能被匹配。
当使用正则或者是数组时,要记得使用v-bind 。
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
③ max:定义组件的最大缓存个数。
内部采用LRU算法用于维护缓存列表。如果缓存个数超过最大数,那么会将最久没有被访问到的组件移出缓存列表,也就是销毁组件。
四、使用 keep-alive 的钩子函数执行顺序问题
keep-alive 提供了两个钩子函数: activated 钩子和 deactivated 钩子。
activated:当组件被激活(使用)的时候触发,即进入这个页面的时候触发,常用于执行一些数据初始化或者异步操作;
deactivated:当组件失活(不被使用)的时候触发,即离开这个页面的时候触发;
① 首次进出组件时:
beforeRouteEnter
> beforeCreate
> created
> mounted
> activated
> ... ... > beforeRouteLeave
> deactivated
② 再次进出组件时:
beforeRouteEnter
> activated
> ... ... > beforeRouteLeave
> deactivated
③ 结论
初始进入和离开 created ---> mounted ---> activated --> deactivated
后续进入和离开 activated --> deactivated
五、应用场景
① 缓存动态组件
② 缓存路由
Vue2写法:
Vue3写法:
在缓存路由的时候,需要借助 v-slot 插槽 + 动态组件的方式,实现路由的缓存。
也可以通过meta属性指定哪些页面需要缓存
③ 实际场景
1)查看表格某条数据的详情页,返回还是之前的状态。比如还是之前的筛选结果,还是之前的页数等;
2)填写的表单的内容路由跳转返回还在。比如 input框、下选择拉框、开关切换等用户输入了一大把东西,跳转再回来不能清空啊,不用让用户再写一遍。
六、原理
keep-alive 是一个缓存,会标记这个虚拟节点被缓存过了。后续就不会重新初始化,也不会进行销毁。
keep-alive
是Vue中内置的一个组件,源码位置:src/core/components/keep-alive.js
解析:
abstract:true,标记这个组件没有任何含义,不需要记录父子关系中。
export default {
name: 'keep-alive',
abstract: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
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.$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
const vnode = getFirstComponentChild(slot)
/* 获取该组件节点的componentOptions */
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
const name = getComponentName(componentOptions)
const { include, exclude } = this
/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
if (
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
/* 获取组件的key值 */
const key = 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
/* 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
}
/* 如果没有命中缓存,则将其设置进缓存 */
else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
可以看到该组件没有
template
,而是用了render
,在组件渲染的时候会自动执行render
函数
this.cache
是一个对象,用来存储需要缓存的组件,它将以如下形式存储:
this.cache = {
'key1':'组件1',
'key2':'组件2',
// ...
}
在组件销毁的时候执行
pruneCacheEntry
函数:
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
/* 判断当前没有处于被渲染状态的组件,将其销毁*/
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
在
mounted
钩子函数中观测include
和exclude
的变化,如下:
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
如果
include
或exclude
发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache
函数,函数如下:
function pruneCache (keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode = cache[key]
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
在该函数内对
this.cache
对象进行遍历,取出每一项的name
值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry
函数将其从this.cache
对象剔除即可。关于
keep-alive
的最强大缓存功能是在render
函数中实现,首先获取组件的key
值:
const key = vnode.key == null?
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
拿到
key
值后去this.cache
对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:
/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
/* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */
remove(keys, key)
keys.push(key)
}
直接从缓存中拿
vnode
的组件实例,此时重新调整该组件key
的顺序,将其从原来的地方删掉并重新放在this.keys
中最后一个。
this.cache
对象中没有该key
值的情况,如下:
/* 如果没有命中缓存,则将其设置进缓存 */
else {
cache[key] = vnode
keys.push(key)
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
表明该组件还没有被缓存过,则以该组件的
key
为键,组件vnode
为值,将其存入this.cache
中,并且把key
存入this.keys
中。此时再判断
this.keys
中缓存组件的数量是否超过了设置的最大缓存数量值this.max
,如果超过了,则把第一个缓存组件删掉。
七、keep-alive存在的问题:缓存后如何获取数据
存在的问题:数据更新问题,缓存的组件重新进入不会再触发created生命周期中的方法,因此数据不会更新。
解决办法:
① 在 beforeRouteEnter 钩子函数中,在路由进入之前先获取数据:
每次组件渲染或者每次进入路由的时候,都会执行 beforeRouteEnter
。
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
② 在 activated 生命周期中,获取数据:
在 keep-alive
缓存的组件被激活的时候,都会执行 actived
钩子。
activated() {
this.getData() // 获取数据
},
注意:服务器端渲染期间 activated
不被调用。
八、总结
总的来说,使用 keep-alive可以有效地提高应用的响应速度和性能,特别是在需要频繁切换组件的情况下。但是在使用 keep-alive 时,要注意数据的更新问题,避免出现数据无法更新的情况。
keep-alive 是一个缓存,会标记这个虚拟节点被缓存过了,后续就不会重新初始化,也不会进行销毁。