这个组件与渲染器联系很深并且要知道渲染器是怎么渲染普通组件,关于渲染器可以看这篇 ,渲染组件看这篇
KeepAlive 的本质是缓存管理,再加上特殊的挂载/卸载逻辑
Vue的 KeepAlive
组件可以避免被它包裹的组件被频繁地 销毁/重建
。 例如
js
<KeepAlive>
<Tab v-if="currentTab === 1">...</Tab>
<Tab v-if="currentTab === 2">...</Tab>
<Tab v-if="currentTab === 3">...</Tab>
</KeepAlive>
如果 Tab组件没有被KeepAlive包裹
,v-if将真实的卸载DOM,挂载DOM。我们都知道这是很消耗性能的。
而且我们可能会遇到一种情况:如果在Tab组件填写了一个表单,然后切换tab,再切换回原来的Tab,可以看到填写的表单数据都消失了。这就是因为组件被卸载在挂载,组件的状态已经被重置
了。
那么 KeepAlive做了什么? 就是将被 KeepAlive 的组件从原容器搬运到另外一个隐藏的容器
中,实现"假卸载
"。当被搬运到隐藏容器中的组件需要再次被"挂载"时,我们也不能执行真正的挂载逻辑,而应该把该组件从隐藏容器中再搬运到原容器
。这个过程对应到组件的生命周期,其实就是 activated
和 deactivated
。
因此 KeepAlive
没有进行卸载操作,只是隐藏了组件,所以组件的状态得以保存,也就实现了缓存
接下来实现一个KeepAlive组件
- 与普通组件相同都是一个对象,但是给它一个独有的属性
__isKeepAlive
,用作标识。 - 并且也有setup函数,首先在setup函数中创建一个
缓存对象
,再获取当前 KeepAlive 组件的实例instance
也就是currentInstance
(currentInstance是一个全局变量,在mountComponent中会先创建一个要挂载组件对应的instance,再把它赋给currentInstance)。 - 因为要把keepalive的内部组件放到一个隐藏容器,之后再把它拿出来,这就需要渲染器创建新元素,移动元素的能力,所以要把渲染器的方法也暴露给组件
- 接着在实例上会被添加两个内部函数,分别是 _deActivate 和 _activate。它们就使用了渲染器的方法对内部组件进行移动。
- 我们之前说过setup也可以返回一个函数作为渲染函数,这里就是返回一个函数。KeepAlive中的内部组件即是它的默认插槽。
- 第一次挂载
将内部组件添加到缓存对象
cache.set(rawVNode.type, rawVNode),如果在缓存对象中能找到,代表应该执行激活,继承组件实例,并打上keepalive的标记。避免重新挂载 - 将 KeepAlive 组件的实例也添加到 vnode 上,以便在渲染器中访问,在组件 vnode 上添加 shouldKeepAlive 属性,并标记为 true,避免渲染器真的将组件卸载
js
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;
};
},
};
接下来考虑内部组件触发unmount
,判断是否是组件并且该组件shouldKeepAlive
为true,代表这个组件不应该被卸载,而是触发_deActivate
。在上面已经把keepalive实例加在了内部组件VNode,所以可以直接调用_deActivate
js
else if (typeof vnode.type === 'object') {
// vnode.shouldKeepAlive 是一个布尔值,用来标识该组件是否应该被 KeepAlive
if (vnode.shouldKeepAlive) {
// 对于需要被 KeepAlive 的组件,我们不应该真的卸载它,而应调用该组件的父组件,
// 即 KeepAlive 组件的 _deActivate 函数使其失活
vnode.keepAliveInstance._deActivate(vnode)
} else {
unmount(vnode.component.subTree)
}
return
}
对应内部组件的挂载,第一次执行setup函数,给缓存对象里加上了内部组件,之后触发setup注册的副作用函数,就可以在缓存对象里找到这个内部组件并他的keptAlive
属性设为true.
这样再次执行patch就可以根据这个属性判断要不要挂载。
js
else if (typeof type === 'object' || typeof type ===
'function') {
// component
if (!n1) {
// 如果该组件已经被 KeepAlive,则不会重新挂载它,而是会调用_activate 来激活它
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container,anchor)
} else {
mountComponent(n2, container, anchor)
}
} else {
patchComponent(n1, n2, anchor)
}
}
最后在mountComponent
将渲染器的方法传给keepalive的实例
js
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,
};
}
// 省略部分代码
}