# 组件的激活与失活
keep-alive
组件的实现需要渲染器层面的支持.被KeepAlive包裹的组件在卸载的时候,我们不能把它真正卸载,而是从原容器搬运到一个隐藏的容器中..
同样,当组件被挂载的时候,执行的也不是真正的挂载逻辑,而是从隐藏的容器搬运到原容器中.这个两个过程对应到组件的生命周期,就是activated
和deactivated
.
一个基本的KeepAlive组件的实现并不困难:
const KeepAlive = {
// KeepAlive组件独有的属性
__isKeepAlive: true,
setup(props, { slots }) {
// 常见一个缓存对象
// key: vnode.type
// value: vnode
const cache = new Map()
// 当前KeepAlive组件的实例
const instance = currentInstance
// KeepAlive组件的实例上存在特殊的keepAliveCtx对象,这个对象是渲染器注入的
// 该对象会暴露渲染器的一些内部方法, move函数是用来将一段dom移动到另一个容器中的
const { move, creatElement } = instance.keepAliveCtx
// 创建隐藏容器
const storageContainer = creatElement('div')
// KeepAlive实例上会被添加两个内部函数,着两个函数会在渲染器中调用
instance._deActivate = (vnode) => {
move(vnode, storageContainer)
}
instance._activate = (vnode, container, anchor) => {
move(vnode, container, anchor)
}
return () => {
// 获取插槽内容
const rawVNode = slots.default()
// 如果不是组件,直接渲染
if (!isVNode(rawVNode)) return rawVNode
// 在挂载之前先获取缓存的vnode
const cachedVNode = cache.get(rawVNode.type)
if(cachedVNode) {
// 命中缓存,继承实例
rawVNode.component = cachedVNode.component
// 添加标记避免渲染器重新挂载
rawVNode.keptAlive = true
} else {
// 没有命中则设置缓存
cache.set(rawVNode.type, rawVNode)
}
// 添加标记避免渲染器卸载
rawVNode.shouldKeepAlive = true
// 将KeepAlive组件实例添加到vnode上,便于渲染器访问
rawVNode.keepAliveInstance = instance
}
}
}
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
- shouldKeepAlive 该属性会被添加在内部组件的vnode上,当渲染器卸载这个组件的时候,可以通过这个检查来判断是否需要卸载组件.
function unmount(vnode) {
if(vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode)
} else {
unmountComponent(vnode)
}
}
1
2
3
4
5
6
7
2
3
4
5
6
7
- keptAlive 该属性会被添加在内部组件的vnode上,当渲染器挂载这个组件的时候,可以通过这个检查来判断是否需要挂载组件.
if(vnode.keptAlive) {
vnode.keepAliveInstance._activate(vnode, container, anchor)
} else {
mountComponent(vnode, container, anchor)
}
1
2
3
4
5
2
3
4
5
- 渲染器注入keepAliveCtx对象
function mountedComponent(vnode, container, anchor) {
const instance = {
state,
props:shallowReactive(props),
isMounted: false,
subTree: null,
slots,
mounted: [],
keepAliveCtx: null
}
const isKeepAlive = vnode.type.__isKeepAlive
if(isKeepAlive) {
// 命中
instance.keepAliveCtx = {
move(vnode, container, anchor) {
// 将组件渲染的内容移动到指定容器中
insert(vnode.component.subTree.el, container, anchor)
},
creatElement
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# KeepAlive的include和exclude属性
在默认情况下,KeepAlive组件会缓存所有的子组件,但是可以通过include和exclude属性来控制哪些组件会被缓存.
include用来显示制定哪些组件会被缓存,exclude用来指定哪些组件不会被缓存.
为了简化问题,我们只允许为include和exclude设置正则表达式.在KeepAlive挂载的时候,会根据内部组件的name进行匹配.
const cache = new Map()
const KeepAlive = {
__isKeepAlive: true,
props: {
include: RegExp,
exclude: RegExp
},
setup(props, { slots }) {
// ...
return () => {
const rawVNode = slots.default()
if (!isVNode(rawVNode)) return rawVNode
// 获取name
const name = rawVNode.type.name
if (name && (
(props.include && !props.include.test(name)) ||
(props.exclude && props.exclude.test(name))
)) {
// 无法被匹配
return rawVNode
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 缓存处理
前文给出的实现中,我们使用一个Map对象来实现对组件的缓存.
如果缓存不存在的时候,我们总是设置新的缓存.这会导致缓存不断增加,极端情况下会占用大量内存.我们必须设置一个缓存阀值.当缓存超过阀值时,我们要对缓存进行修剪.
Vue3所采取的修剪策略是 "最新一次访问". 首先我们为缓存设置一个最大容量,通过max属性设置.
<keep-alive :max="2">
<component :is="component"></component>
</keep-alive>
1
2
3
2
3
我们设置了最大缓存量为2,假设我们有三个组件,并且他们都会被缓存,我们模拟一下组件切换过程中的缓存变化
- 初始渲染Comp1并缓存,此时缓存队列为[Comp1], 并且最新一次访问的组件是Comp1
- 切换到Comp2,此时缓存队列为[Comp1, Comp2], 最新一次访问的组件是Comp2
- 切换到Comp3,此时缓存队列已满,需要修剪缓存.因为当前最新一次访问的组件是Comp2.所以它是安全的,即Comp1组件的缓存会被修剪,修剪完毕后的空间会用来存储Comp3的缓存.所以现在的缓存队列是[Comp2, Comp3], 并且最新一次访问的组件是Comp3
Vue3缓存策略的核心在把当前访问的组件作为最新一次访问的组件, 并且该组件在缓存修剪过程中始终是安全的.