keep-alive
- 主要用于保留组件状态或避免重新渲染
<keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。- 和
<transition>相似,<keep-alive>是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。 - 当组件在
<keep-alive>内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。 - 在 2.2.0 及其更高版本中,activated 和 deactivated 将会作用在
<keep-alive>的直接子节点及其所有子孙节点 <keep-alive>只用在其一个直属的子组件被切换的情形,如果有多个子元素同时存在,则不会工作
include 和 exclude
- 允许组件有条件地缓存,二者都可以用逗号分隔字符串、正则表达式、一个数组来表示
- 首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称(父组件 components 选项的键值)
- 匿名组件不能被匹配
- 不会再函数式组件中工作,因为它们没有缓存实例
max
最多可以缓存多少组件实例,一旦达到这个数字,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁
源码剖析
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判断当前组件虚拟 dom 是否渲染成真实 dom 的关键
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number],
},
created() {
this.cache = Object.create(null);
this.keys = [];
},
destroyed() {
for (const key in this.cache) {
// 删除所有缓存
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted() {
// 实时监听黑白名单的变动,实时地更新或删除 this.cache 对象数据
this.$watch('include', valu => {
pruneCache(this, name => matched(val, name));
});
this.$watch('exclude', valu => {
pruneCache(this, name => !matched(val, name));
});
},
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;
if (
// 条件匹配
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode;
}
const { cache, keys } = this;
// 定义组件的缓存 key
const key =
vnode.key === null
? componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key;
if (cache[key]) {
// 已缓存过该组件
vnode.componentInstance = cache[key].componentInstance;
remove(keys, key);
keys.push(key); // 调整 key 排序
} else {
cache[key] = vnode; // 缓存组件对象
keys.push(key);
if (this.max && keys.length > parseInt(this.max)) {
// 超过缓存数限制,将删除第一个
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keyAlive = true; // 渲染和执行被包裹组件的钩子函数需要用到
}
return vnode || (slot && slot[0]);
},
};
function pruneCacheEntry(
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cache = cache[key];
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroyed(); // 执行组件的 destroy 钩子函数
}
cache[key] = null;
remove(keys, key);
}
function pruneCache(keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance;
for (const key in cache) {
const cacheNode = cache[key];
if (cachedNode) {
const name = getComponentName(cacheNode.componnentOptions);
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode);
}
}
}
}
流程解析
- 获取
<keep-alive>包裹着的第一个子组件对象及其组件名 - 根据设定的黑白名单进行条件匹配,决定是否缓存,不匹配则直接返回组件实例,否则执行下一步
- 根据组件 id 和 tag 生成缓存 key,并在缓存对象中查找是否已缓存过该组件实例,如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键),否则执行下一步(LRU:页面置换算法,将最近最久未使用的页面予以淘汰)
- 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即下标为 0 的那个 key)
- 最后,将该组件实例的 keepAlive 属性值设置为 true
渲染
vue 渲染的整个过程

vue 的渲染是从图中的 render 阶段开始,但 keep-alive 的渲染是在 patch 阶段,这是构建组件树(虚拟 DOM 树),并将 VNode 转换成真正 DOM 节点的过程
keep-alive 组件的渲染
// src/core/instance/lifecycle.js
export function initLifecycle(vm) {
const options = vm.$options;
// 找到第一个非 abstract 父组件实例
let parent = options.parent;
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
vm.$parent = parent;
}
vue 在初始化生命周期的时候,为组件实例建立父子关系时,会根据 abstract 属性决定是否忽略某个组件。在<keep-alive>中,设置了abstract: true,那么 vue 就会跳过该组件实例,因此最后构建的组件树中就不包含<keep-alive>组件及相关的节点
在 patch 阶段,会执行 createComponent 函数,将被<keep-alive>包裹的组件插入父元素中
- 首次加载被包裹组件时,由 keey-alive.js 中的 render 函数可知,vnode.componentInstance 的值为 undefined,keepAlive 的值为 true,因为
<keep-alive>组件作为父组件,它的 render 函数会先于被包裹组件执行,因此只执行到i(vnode, false),后面的逻辑不执行 - 再次访问被包裹组件时,vnode.componentInstance 为已经缓存的组件实例,那么会执行
insert(parentElem, vnode.elem, refElem)逻辑,这样就直接把上一次的 DOM 插入到父元素中
// src/core/vdom/patch.js
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data;
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef((i = i.hook)) && isDef((i = i.init))) {
i(vnode, false);
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElem, vnode.elem, refElem); // 将缓存的 DOM(vnode.elm) 插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(
vnode,
insertedVnodeQueue,
parentEle,
refElm
);
}
return true;
}
}
}
钩子函数
一般的组件每次加载都会有完整的生命周期,因为被缓存的组件实例会被设置 keepAlive=true,同时在初始化组件钩子函数中有如下逻辑判断,因此,当 vnode.componentInstance 和 keepAlive 同时为 true 时,不再进入$mounte 过程,mounted 之前的所有钩子函数都不再执行
// src/core/vdom/create-component.js
const componentVNodeHooks = {
init(vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// keep-alive components, treat as a patch
const mountedNode = vnode;
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
const child = (vnode.componentInstance =
createComponentInstanceForVnode(vnode, activeInstance));
}
},
};
activated 和 deactivated
- activated:在 patch 的阶段,最后会执行 invokeInserHook 函数,而这个函数就是去调用组件实例自身的 insert 钩子
- deactivated:也是一样的原理,在组件实例(VNode)的 destroy 钩子函数中调用 deactivateChildComponent 函数。
// src/core/vdom/patch.js
function invokeInsertHook(vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue;
} else {
for (let i = 0; i < queue.length; i++) {
queue[i].data.hook.insert(queue[i]);
}
}
}
在 insert 钩子里面,调用了 activateChildComponent 函数递归地区执行所有子组件的 activated 钩子函数
// src/cord/vdom/create-component.js
const componentVNodeHooks = {
insert(vnode) {
const { context, componentInstance } = vnode;
if (!componentInstance._isMounted) {
componentInstance._isMounted = true;
callHook(componentInstance, 'mounted');
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance);
} else {
activateChildComponent(componentInstance, true);
}
}
},
};
// src/core/instance/lifecycle.js
export function activateChildComponent(vm, direct) {
if (direct) {
vm._directInactive = false;
if (isInInactiveTree(vm)) {
return;
}
} else if (vm._directInactive) {
return;
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false;
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i]);
}
callHook(vm, 'activated');
}
}