0%

keepAlive原理

原理:

将被KeepAlive的组件从原容器搬运到另一个隐藏的容器中,实现“假卸载“,当被搬运到隐藏容器中的组件需要再次被挂载时,我们也不能执行真正的挂载逻辑,而是把组件从隐藏容器中再搬运到原容器,这个过程对应到组件的生命周期,就是activated和deactivated

一个简单的KeepAlive组件:

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
50
51
52
53
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, createElement } = instance.KeepAliveCtx;

//创建隐藏容器
const storageContainer = createElement("div");

//keepAlive组件实例上会被添加两个内部函数,分别是_deActivate和_activate
//这两个函数会在渲染器中被调用
instance._deActivate = (vnode) => {
move(vnode, storageContainer);
};
instance._activate = (vnode, container, anchor) => {
move(vnode, container, anchor);
};
return () => {
//keepAlive的默认插槽就是要被keepAlive的组件
let rawVNode = slots.default();
//如果不是组件,直接渲染,因为非组件的虚拟结点无法被keepAlive
if (typeof rawVNode.type !== "object") {
return rawVNode;
}
//在挂载时先获取缓存的组件vnode
const cachedVNode = cache.get(rawVNode.type);
if (cachedVNode) {
//如果有缓存的内容,说明不应该执行挂载,应该执行激活
//继承组件实例
rawVNode.component = cachedVNode.component;
//在vnode上添加keptAlive属性,标记为true,避免渲染器重新挂载
rawVNode.keptAlive = true;
} else {
//如果没有缓存,则将其添加到缓存中,这样下次激活组件时不会执行新的挂载动作
cache.set(rawVNode.type, rawVNode);
}
//在组件vnode上添加shouldKeepAlive属性,并标记为true,避免渲染器将组件卸载
rawVNode.shouldKeepAlive = true;
//将keepAlive组件的实例也添加到vNode上,以便在渲染器中访问
rawVNode.KeepAliveInstance = instance;
//渲染组件 vnode
return rawVNode;
};
},
};

KeepAlive组件会对内部组件进行操作,主要是在内部组件的vnode对象上添加一些标记属性,以便渲染器能够执行特定的逻辑,这些标记属性包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function unmount(vnode) {
if(vnode.type === Fragment) {
vnode.children.forEach(c=>unmount(c))
return
}else if(typeof vnode.type === 'object') {
//vnode.shouldAlive是一个布尔值,用来标识该组件是否应该被KeepAlive
if(vnode.shouldKeepAlive) {
//对于需要被keppAlive的组件,我们不应该直接卸载,而应该调用该组件的父组件,即keepAlive的父组件_deActive函数使其失活
vnode.KeepAliveInstance._deActivate(vnode)
}else {
unmount(vnode.component.subTree)
}
return
}
const parent = vnode.el.parentNode
if(parent) {
parent.removeChild(vnode.el)
}
}

shouldKeepAlive:该属性会被添加到内部组件的vnode对象,这样当渲染器卸载内部组件时,可以通过检查该属性得知,内部组件需要被KeepAlive,于是渲染器不会真的卸载内部组件,而是会调用_deActivate函数完成搬运工作

keptAlive:内部组件如果已经被缓存,则还会为其添加一个keptAlive标记,这里当内部之间需要重新渲染时,渲染器并不会重新挂载它,而会将其激活

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
function patch(n1,n2,container,anchor) {
if(n1 && n1.type !== n2.type) {
unmount(n1)
n1=null
}
const {type} = n2
if(typeof type === 'string'){

}else if(type === Text){

}else if(type === Fragment){

}else if(typeof type === 'object' || typeof type === 'function') {
if(!n1) {
//如果该组件已经被keepAlive,则不会重新挂载它,而是会调用_active来激活它
if(n2.keptAlive){
n2.keepAliveInstance._activate(n2,container,anchor)
}else {
mountComponent(n2,container,anchor)
}
}else {
patchComponent(n1,n2,anchor)
}
}

}

失活的本质就是将组件所渲染的内容移动到隐藏容器中,激活的本质是将组件所渲染的内容搬运到原来的容器

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
//涉及move的函数由渲染器注入的
function mountComponent(vnode,container,anchor) {
//...
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots,
mounted: [],
//只有KeepAlive组件的实例下会有keepAliveCtx属性
keepAliveCtx: null
}
//检查当前要挂载的组件是否是KeepAlive组件
const isKeepAlive = vnode.type.__isKeepAlive
if(isKeepAlive) {
//在KeepAlive组件实例上添加keepAliveCtx对象
instance.keepAliveCtx = {
//move函数用来移动一段vnode
move(vnode,container,anchor) {
//本质上是将组件渲染的内容移动到指定容器中,即隐藏容器中
insert(vnode.component.subTree.el,container,anchor)
},
createElement
}
}//...
}

用LRU算法实现缓存管理,可以为缓存设置最大容量

1
2
3
<KeepAlive :max="2">
<component :is="dynamicComp"></component>
</KeepAlive>

LRU算法的思想就是会把当前访问(或渲染)的组件作为最新一次渲染的组件,移动到队头,而如果队列容量不够,会把最久未被使用的组件即队尾组件移出队列,也可以自定义缓存实例:

1
2
3
<KeepAlive :cache="cache">
<component :is="dynamicComp"></component>
</KeepAlive>