keep-alive是什么?

keep-alive 是 Vue 中内置的一个抽象组件,用于缓存动态组件或者路由,本身不会被渲染,在组件切换时,缓存其包裹的组件的状态,使其不被销毁,防止多次渲染.

keep-alive的应用场景

用户在某个列表页面选择筛选条件过滤出一份数据列表,由列表页面进入数据详情页面,再返回该列表页面,我们希望:列表页面可以保留用户的筛选(或选中)状态。

keep-alive的原理

keep-alive 是 Vue 中内置的一个抽象组件,用于缓存动态组件或者路由,本身不会被渲染。

其原理如下:
1、keep-alive本身不会渲染出来,也不会出现在父组件链中,keep-alive包裹动态组件或路由。
1、当动态组件路由首次渲染时,keep-alive会将其VNode缓存起来,并将其从虚拟 DOM 树中移除。
2、如果动态组件路由被缓存,再次渲染时就不会重新创建和挂载VNode,只需直接从缓存中取出VNode,并将其挂载到对应的位置上即可。
3、当缓存中的动态组件路由被离开时,keep-alive不会销毁实例,而是将其保存到缓存中。如果缓存中的实例数超过max值,会触发LRU淘汰策略,将最近未使用的实例销毁。

1
2
3
4
5
6
7
8
const key = componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')

// componentOptions.Ctor.cid 表示当前组件的唯一标识符

// componentOptions.Ctor.cid 表示的是组件构造函数的唯一标识符,这个值在组件创建时就已经生成。这个值是通过 Vue 内部的组件注册机制给每个组件分配的,在组件定义时生成。在组件渲染时,Vue 会通过这个值来判断当前组件是否已经存在、是否需要重新渲染等。


// componentOptions.tag 则表示当前组件使用的 HTML 标签名或组件名称。

首次渲染动态组件或路由时,为什么将其从DOM中移除?

操作步骤如下:

1、keep-alive监听动态组件或路由的activateddeactivated生命周期钩子函数。
2、当动态组件或路由被激活时,也就是进入缓存阶段时,activated钩子函数会被调用,这是keep-alive会将当前的实例的VNode存储起来,并将其从父组件的虚拟DOM树中移除该节点,并不会销毁其实例。
3、当缓存中的组件或路由被激活时,activated钩子函数会被调用,keep-alive会从缓存中将当前的实例的VNode取出,并将其渲染载对应的位置上。

通过缓存和移除动态组件或者路由实例,keep-alive 可以减少组件的初始加载时间页面的加载时间,提高用户体验。

为什么将其从DOM中移除?
keep-alive 将动态组件或者路由从 DOM 中移除的原因是为了减少不必要的渲染,从而提高页面渲染的性能和流畅度。

在 Vue 中,只要一个组件被渲染,就会生成一棵组件树。每个组件都会对应一颗虚拟 DOM 树,在组件树中包含了该组件的子组件以及所有状态的数据。当组件的状态改变时,组件树会重新构建,所有的组件和子组件都会被重新渲染。

  • 不移除: 那么在组件状态发生变化时,VNode会被重新渲染.
  • 移除: 只需要重新渲染组件树的一部分,这样就可以降低不必要的渲染,提高性能和流畅度.

keep-alive的属性

  • include: 用于指定哪些组件或路由需要缓存。其值为一个字符串或正则表达式,表示匹配到的组件或路由都将被缓存.

    1
    2
    $ :include="'component1|component2'"
    $ :include="/comp.*/"
  • exclude: 用于指定哪些组件或路由不缓存。使用方法与include相同。

  • max: 用于指定最大缓存数。其值为一个数字类型,表示同一时间最多可以缓存的组件/路由实例数,超过最大值,采用LRU淘汰策略,删除实例。
    1
    $ :max="10"

keep-alive嵌套路由和动态组件的使用方法

Vue3中keep-alive结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive

路由

1
2
3
4
5
6
7
8
9
10
// Vue3.x
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
// Vue2.x
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
<router-view></router-view>
</keep-alive>

动态组件

1
2
3
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount"> 
<component :is="currentComponent"></component>
</keep-alive>

keep-alive缓存后如果要获取数据的方式

beforeRouteEnter

在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter

1
2
3
4
5
6
7
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
}

actived
在keep-alive缓存的组件被激活的时候,都会执行actived钩子

1
2
3
activated(){
this.getData() // 获取数据
}

keep-alive怎么缓存组件的,缓存后又是怎么更新的?

1、结合属性includeexclude可以明确指定缓存哪些组件或排除缓存指定组件。
2、keep-alive是一个通用组件,它的内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的 VNode,如果该组件在map中找到它就会直接返回,由于componentis属性是个响应式数据,因此只要它变化,keep-alive内部的render函数就会重新执行。

keep-alive包裹的组件是如何使用缓存的

  1. 首次加载被包裹组件时,在keep-alive.js中的render函数可知,VNode.componentInstance的值是undfined,keepAlive的值是true,因为keep-alive组件作为父组件,它的render函数会先于被包裹组件执行;由于abstaract为true那么后面的逻辑不执行;
  2. 再次访问被包裹组件时,VNode.componentInstance的值就是已经缓存的组件实例,那么会执行insert(parentElm, VNode.elm, refElm)逻辑,这样就直接把上一次的DOM插入到父元素中.

keep-alive的生命周期?

activated激活
activated: 页面进入的时候会触发

页面第一次进入的时候,钩子函数触发的顺序是: created->mounted->activated
当再次前进或者后退的时候只触发:activated

deactivated离开

deactivated: 页面退出的时候会触发

keep-alive的原理&源码解析

keep-alive在生命周期钩子函数中操作

  • created: 初始化一个cachekeyscache用来存缓存组件的虚拟DOM集合,keys用来存缓存组件的key集合.
  • mounted:实时监听includeexclude这两个的变化,并执行相应操作.
  • destroyed: 删除掉所有缓存相关的东西.

源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/core/components/keep-alive.js

export default {
name: 'keep-alive',
abstract: true, // 判断此组件是否需要在渲染成真实DOM
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created() {
this.cache = Object.create(null) // 创建对象来存储 缓存虚拟dom
this.keys = [] // 创建数组来存储 缓存key
},
mounted() {
// 实时监听include、exclude的变动
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
destroyed() {
for (const key in this.cache) { // 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
render() {
// 下面讲
}
}

pruneCacheEntry函数
1、遍历集合,执行所有缓存组件的$destroy方法
2、将cache对应key的内容设置为null
3、删除keys中对应的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
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() // 执行组件的destory钩子函数
}
cache[key] = null // 设为null
remove(keys, key) // 删除对应的元素
}

render函数

1、获取到keep-alive包裹的第一个组件以及它的组件名称
2、判断此组件名称是否能被白名单、黑名单匹配,如果不能被白名单匹配 || 能被黑名单匹配,则直接返回VNode,不往下执行,如果不符合,则往下执行第三步
3、根据组件ID、tag生成缓存key,并在缓存集合中查找是否已缓存过此组件。如果已缓存过,直接取出缓存组件,并更新缓存key在keys中的位置(这是LRU算法的关键),如果没缓存过,则继续第四步
4、分别在cache、keys中保存此组件以及他的缓存key,并检查数量是否超过max,超过则根据LRU算法进行删除。
5、将此组件实例的keepAlive属性abstract设置为true.

keep-alive本身渲染

Vue在初始化生命周期的时候,为组件实例建立父子关系会根据abstract属性决定是否忽略某个组件。在keep-alive中,设置了abstract:true,那Vue就会跳过该组件实例.
最后构建的组件树中就不会包含keep-alive组件,那么由组件树渲染成的DOM树自然也不会有keep-alive相关的节点了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/core/instance/lifecycle.js

export function initLifecycle (vm: Component) {
const options = vm.$options
// 找到第一个非abstract的父组件实例
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}

结束语


总结:大功告成✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️

参考链接: