keep-alive 是 Vue 提供的一个内置组件,用于缓存不活动的组件实例,而不是销毁它们。这样可以在组件切换时保留其状态或避免重新渲染。下面我们详细讲解 keep-alive 的原理,并对 Vue 2 和 Vue 3 的实现进行比较。
Vue 2 的 keep-alive 原理
在 Vue 2 中,keep-alive 组件通过包裹动态组件来实现缓存功能。其核心逻辑在于将不活动的组件实例缓存起来,当需要重新激活时,从缓存中取出,而不是重新创建实例。
Vue 2 实现代码(简化版)
以下是 Vue 2 中 keep-alive 的核心代码(简化版):
const KeepAlive = {
name: 'keep-alive', // 定义组件的名字为 keep-alive
abstract: true, // 将组件标记为抽象组件。抽象组件不会在父组件链中出现。
props: {
include: [String, RegExp, Array], // include:指定哪些组件需要被缓存,可以是字符串、正则表达式或数组。
exclude: [String, RegExp, Array], // 指定哪些组件不需要被缓存,可以是字符串、正则表达式或数组
max: [String, Number] // 缓存组件的最大数量
},
created() {
this.cache = Object.create(null); // 用于存储缓存的组件实例。
this.keys = []; // 用于存储缓存组件的键列表,按顺序保存。
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this._vnode); // 遍历 this.cache 中的所有键,并调用 pruneCacheEntry 函数清理缓存
}
},
mounted() {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name)); // 监听 include 属性的变化,并调用 pruneCache 函数清理不匹配的缓存。
});
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name)); // 监听 exclude 属性的变化,并调用 pruneCache 函数清理匹配的缓存。
});
},
render() { // 渲染函数,定义组件的渲染逻辑。
const slot = this.$slots.default; // 获取默认插槽内容。
const vnode = getFirstComponentChild(slot); // 获取插槽内容中的第一个子组件虚拟节点。
const componentOptions = vnode && vnode.componentOptions; // 获取子组件的选项。
if (componentOptions) { // 子组件的选项 存在时,继续处理。
const name = getComponentName(componentOptions);
const { include, exclude } = this;
/**
* 检查 include 和 exclude 属性。
* 如果 include 存在且组件名称不匹配,则直接返回 vnode。
* 如果 exclude 存在且组件名称匹配,则直接返回 vnode。
*/
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode;
}
/**
* 生成缓存键 key。
* 如果 vnode.key 为空,则使用组件构造函数的 cid 和标签名生成键。
* 否则,使用 vnode.key 作为键。
*/
const key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key;
/**
* 检查缓存中是否存在该键。
* 如果存在,从缓存中取出组件实例,并将键移到 this.keys 的末尾。
* 如果不存在,将组件虚拟节点存入缓存,并将键添加到 this.keys。
* 如果缓存数量超过 max,则清理最早的缓存。
*/
if (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance;
remove(this.keys, key);
this.keys.push(key);
} else {
this.cache[key] = vnode;
this.keys.push(key);
if (this.max && this.keys.length > parseInt(this.max)) {
pruneCacheEntry(this.cache, this.keys[0], this._vnode);
}
}
vnode.data.keepAlive = true; // 标记虚拟节点为 keepAlive
}
return vnode || (slot && slot[0]); // 返回虚拟节点或插槽中的第一个子节点
}
};
/**
* 清理缓存条目。
* 获取缓存条目 entry。
* 如果存在且不等于当前虚拟节点,销毁组件实例。
* 将缓存条目置为空。
*/
function pruneCacheEntry(cache, key, current) {
const entry = cache[key];
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy();
}
cache[key] = null;
}
/**
* 根据过滤条件清理缓存。
* 获取 cache、keys 和 _vnode。
* 遍历缓存,获取组件名称。
* 如果名称存在且不符合过滤条件,清理缓存条目。
*/
function pruneCache(keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance;
for (const key in cache) {
const entry = cache[key];
if (entry) {
const name = getComponentName(entry.componentOptions);
if (name && !filter(name)) {
pruneCacheEntry(cache, key, _vnode);
}
}
}
}
/**
* 检查名称是否匹配模式。
* 如果模式是数组,检查名称是否在数组中。
* 如果模式是字符串,拆分字符串并检查名称是否在其中。
* 如果模式是正则表达式,测试名称是否匹配。
* 否则返回 false。
*/
function matches(pattern, name) {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1;
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1;
} else if (pattern.test) {
return pattern.test(name);
}
return false;
}
// 获取组件构造函数的 name 或标签名
function getComponentName(opts) {
return opts && (opts.Ctor.options.name || opts.tag);
}
// 获取第一个子组件 过滤并返回第一个有 componentOptions 的子节点。
function getFirstComponentChild(children) {
return children && children.filter(c => c && c.componentOptions)[0];
}
Vue 3 的 keep-alive 原理
在 Vue 3 中,keep-alive 组件的实现有所变化,主要是因为 Vue 3 使用了新的虚拟 DOM 和响应式系统。不过,核心思想仍然是缓存组件实例。
Vue 3 实现代码(简化版)
以下是 Vue 3 中 keep-alive 的核心代码(简化版):
import { defineComponent, onMounted, onUnmounted, ref, watch, getCurrentInstance } from 'vue';
const KeepAlive = defineComponent({
name: 'KeepAlive',
props: {
include: [String, RegExp, Array], // 指定需要缓存的组件,可以是字符串、正则表达式或数组。
exclude: [String, RegExp, Array], // 指定不需要缓存的组件,可以是字符串、正则表达式或数组。
max: [String, Number] // 缓存组件的最大数量
},
setup(props, { slots }) {
const cache = new Map(); // 用 Map 存储缓存的组件实例
const keys = new Set(); // 用 Set 存储缓存组件的键
let current = null; // 当前激活的组件实例
// 返回组件选项的 name 或 __name
function getComponentName(opts) {
return opts && (opts.name || opts.__name);
}
// 查找并返回第一个有 type 且 type.__isKeepAlive 为 true 的子节点
function getFirstComponentChild(children) {
return children && children.find(c => c.type && c.type.__isKeepAlive);
}
const instance = getCurrentInstance(); // 获取当前组件实例
// 定义激活函数
instance.ctx.activate = (vnode, container, anchor) => {
// move 函数通常定义在渲染器的实现中 用于将虚拟节点(VNode)移动到指定的 DOM 容器中
move(vnode, container, anchor); // 将虚拟节点移动到容器中
/**
* 在渲染后执行的队列中添加函数。
* 设置组件为激活状态。
* 如果组件有激活钩子函数(a),则调用它们。
*/
queuePostRenderEffect(() => {
vnode.component.isDeactivated = false;
if (vnode.component.a) {
invokeArrayFns(vnode.component.a);
}
}, parentSuspense);
};
// 定义停用函数
instance.ctx.deactivate = (vnode) => {
move(vnode, storageContainer, null); // 将虚拟节点移动到存储容器中
/**
* 在渲染后执行的队列中添加函数。
* 设置组件为停用状态。
* 如果组件有停用钩子函数(da),则调用它们。
*/
queuePostRenderEffect(() => {
vnode.component.isDeactivated = true;
if (vnode.component.da) {
invokeArrayFns(vnode.component.da);
}
}, parentSuspense);
};
/**
* 清理缓存条目
* 获取缓存的组件实例。
* 如果存在,设置组件为停用状态。
* 如果组件有停用钩子函数(da),则调用它们。
* 从缓存和键集合中删除该条目。
*/
function pruneCacheEntry(key) {
const cached = cache.get(key);
if (cached) {
cached.component.isDeactivated = true;
if (cached.component.da) {
invokeArrayFns(cached.component.da);
}
cache.delete(key);
keys.delete(key);
}
}
/**
* 监听 include 和 exclude 属性的变化。
* 遍历缓存,获取组件名称。
* 如果名称不符合 include 或符合 exclude,则清理缓存条目。
*/
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
cache.forEach((vnode, key) => {
const name = getComponentName(vnode.type);
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
pruneCacheEntry(key);
}
});
},
{ flush: 'post', deep: true }
);
return () => {
const children = slots.default(); // 获取默认插槽内容。
const vnode = getFirstComponentChild(children) // 获取插槽内容中的第一个子组件虚拟节点;
const componentOptions = vnode && vnode.type // 获取子组件的选项;
if (componentOptions) {
const name = getComponentName(componentOptions); // 获取子组件的名称
const { include, exclude } = props; // 解构 include 和 exclude 属性
/**
* 检查 include 和 exclude 属性。
* 如果 include 存在且组件名称不匹配,则直接返回 vnode。
* 如果 exclude 存在且组件名称匹配,则直接返回 vnode
*/
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode;
}
/**
* 生成缓存键 key
* 如果 vnode.key 为空,则使用组件选项作为键 否则,使用 vnode.key 作为键
*/
const key = vnode.key == null ? componentOptions : vnode.key;
/**
* 检查缓存中是否存在该键。
* 如果存在,从缓存中取出组件实例,并将键移到 keys 的末尾。
* 如果不存在,将组件虚拟节点存入缓存,并将键添加到 keys。
* 如果缓存数量超过 max,则清理最早的缓存。
* 标记虚拟节点为 keepAlive。
* 返回虚拟节点。
*/
if (cache.has(key)) {
vnode.component = cache.get(key).component;
keys.delete(key);
keys.add(key);
} else {
cache.set(key, vnode);
keys.add(key);
if (props.max && keys.size > parseInt(props.max, 10)) {
pruneCacheEntry(Array.from(keys)[0]);
}
}
vnode.shapeFlag |= 256;
}
return vnode;
};
}
})
export default KeepAlive;
-
缓存机制:
- Vue 2 使用了对象字面量和数组来管理缓存。
- Vue 3 使用了
Map和ref来管理缓存和键列表。
-
代码结构:
- Vue 2 的
keep-alive组件是使用对象语法定义的。 - Vue 3 则使用了
defineComponent和组合式 API。
- Vue 2 的