1.keepAlive组件的使用
默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态------当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。
<KeepAlive>
是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例.假设我们再页面有一组Tab组件,如下代码:
js
<template>
<Tab v-if="currentTab === 1">...</Tab>
<Tab v-if="currentTab === 2">...</Tab>
<Tab v-if="currentTab === 3">...</Tab>
</template>
当我们根据变量currentTab
值不同,会渲染不同的组件,当我们频繁的切换Tab时,会导致不停的卸载并重建组件,为了避免因此产生的性能开销。于是就有了keepAlive这个组件。
那么keepAlive组件实现的原理是怎么样的呢?从本质上面来说KeepAlive就是缓存管理,结合上特殊的卸载和挂载逻辑 ,KeepAlive的实现跟渲染层是密切相关的,我们可以理解为卸载keepAlive时,我们不是真的卸载,我们只是把它送到了一个隐藏的容器中,当再次挂载时,只是把那个组件从隐藏容器中搬出来,这就涉及到了组件的生命周期,activated
和deactivated
。
2.KeepAlive的基本实现
当我们理解到了KeepAlive组件,它并不会真正的挂载和卸载后,只是隐藏到一个容器中,然后再取出来的过程后,我们就可以简单的实现一下逻辑了:
js
const KeepAlive = {
_isKeepAlive: true,
setup(props, { slots }) {
const cache = new Map() //缓存组件实例
const instance = currentInstance;
const { move, createElement } = instance.keepAliveCtx
//创建隐藏容器
const storageContainer = createElement('div')
//组件上面的实力上会被添加两个内部函数,一个是_deactivate,一个是_activate,分别表示组件被激活和被隐藏
instance._deactivate = (vnode) => {
move(vnode, storageContainer)
}
instance._activate = (vnode, container, anchor) => {
move(vnode, container, anchor)
}
return () => {
const rawVNode = slots.default() //获取插槽的内容
if (typeof rawVNode.type !== 'object') { //如果不是组件,直接渲染,非组件的的虚拟节点无法被keepAlive
return rawVNode
}
const cachedVNode = cache.get(rawVNode.type) //再挂载时先获取缓存的组件vnode
if (cachedVNode) {
rawVNode.component = cachedVNode.component
rowVNode.keptAlive = true //表示这个组件是被keepAlive的,避免渲染器重新挂载
} else {
cache.set(rawVNode.type, rawVNode)
}
rawVNode.shouldKeepAlive = true //再组件的vnode上添加属性,避免渲染器将组件卸载
rawVNode.keepAliveInstance = instance
return rawVNode
}
}
}
从上面实现中可以看到以下几点
- KeepAlive的实现跟渲染器结合非常深,KeepAlive组件本身不会渲染额外的内容,它最终返回是需要缓存的组件
- 由KeepAlive包裹的组件,我们称之为"内部组件",相当于给这些组件打上了标识
- KeepAlive会在内部对这些缓存组件添加一些标记属性,以便再渲染器中执行特定的逻辑
2.1 shouldKeepAlive
shouldKeepAlive
:该属性就是在卸载时,告诉渲染器,这是需要被KeepAlive的组件,不能执行真正的卸载逻辑,调用_deactivate
完成搬运即可,代码如下:
js
//卸载操作
function unmount(vnode) {
if (vnode.type === Fragment) {
vnode.children.forEach(c = unmount(c))
} else if (typeof vnode.type === 'object') {
if (vnode.shouldKeepAlive) { //判断组件是否是keepAlive组件,是的话,直接隐藏
vnode.keepAliveInstance._deactivate(vnode)
} else {
unmount(vnode.component.subTree) //如果不是keepAlive的组件,直接卸载
}
}
}
可以看到,unmount 函数在卸载组件时,会检测组件是否应该被 KeepAlive,从而执行不同的操作。
2.2keptAlive
keptAlive
:组件如果已经被缓存,则还会为其添加一个 keptAlive 标记。这样"内部组件"需要重新渲染时,渲染器并不会重新挂载它,而会将其激活,如下面 patch 函数的代码所示:
js
//挂载激活操作
function patch(n1, n2, anchor) {
//省略其他代码
const { type } = n2
if (typeof type === 'object' || typeof type === 'function') {
if (!n1) {
//如果组件已经被keepAlive了,直接激活
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, n2.el, anchor) //调用keepAlive的_activate方法
}
}
}
}
可以看到,如果组件的 vnode 对象中存在 keptAlive标识,则 渲染器不会重新挂载它,而是会通过keepAliveInstance._activate 函数来激活它。
2.3 deActivate和activate实现
失活的本质就是将组件所渲染的内容移动到隐藏容器中,而激活的本质是将组件所渲染的内容从隐藏容器中搬运回原来的容器。当我们实现渲染的时候,就可以再在KeepAlive 组件实例上添加 keepAliveCtx 对象,添加上movde方法,实现组件的隐藏和现实。代码如下:
js
function mountComponent(vnode, container, anchor) {
const instance = {
state,
keepAliveCtx: null, //只有再keepAlive组件的实例下会有keepAliveCtx
}
//检查当前要挂载的组件是否是keepAlive组件
if (_isKeepAlive) {
//再keepAlive组件的实例下挂载keepAliveCtx
instance.keepAliveCtx = {
move(vnode, container, anchor) {
//本质是将组件渲染的内容移动到指定的容器内,隐藏容器
insert(vnode.component.subTree.el, container, anchor)
},
createElement() {
//创建元素
}
}
}
}
3.include和exclude的实现
默认情况下,KeepAlive组件会对所有的"内部组件"进行缓存,但我们期待可以自定义规则,所以就由include和exclude。include 用来显式地配置应该被缓存组件,而 exclude 用来显式地配置不应该被缓存组件。
KeepAlive 组件的 props 定义如下:
js
const KeepAlive = {
__isKeepAlive: true,
// 定义 include 和 exclude
props: {
include: RegExp,
exclude: RegExp
},
setup(props, { slots }) {
// 省略部分代码
}
}
为了简化问题,我们只允许为 include 和 exclude 设置正则类 型的值。在 KeepAlive 组件被挂载时,它会根据"内部组件"的名称(即 name 选项)进行匹配,如下面的代码所示:
js
const cache = new Map()
const KeepAlive = {
__isKeepAlive: true,
props: {
include: RegExp,
exclude: RegExp
},
setup(props, { slots }) {
// 省略部分代码
return () => {
let rawVNode = slots.default()
if (typeof rawVNode.type !== 'object') {
return rawVNode
}
// 获取"内部组件"的 name
const name = rawVNode.type.name
// 对 name 进行匹配
if (
name &&
// 如果 name 无法被 include 匹配
((props.include && !props.include.test(name)) ||
// 或者被 exclude 匹配
(props.exclude && props.exclude.test(name)))) {
// 则直接渲染"内部组件",不对其进行后续的缓存操作
return rawVNode
}
// 省略部分代码
}
我们以正则为例子,对内部组件名称进行了匹配,并根据匹配结果判断是否要进行缓存。include和exclude可以设计成其他规则方法,但无论如何,原理都是不变的