提示:后面有完整调试代码,先运行看下结果再配合文章。
昨天发过一篇,但感觉写的很不好就删了,又花费了几个小时重新写了一篇,这篇完整的解释了keep-alive的运行流程。
本篇应该是本系列最复杂的一篇,作者尽力通俗的表达清楚。
下面进入正题!
Keep-alive是什么?
keep-alive就是将它肚子包裹的节点进行了缓存,一般情况下是一个组件,代码如下:
<keep-alive>
<test-component v-if="isShow">
</test-component>
</keep-alive>
我们第一个要解决的问题就是,<keep-alive>这个标签是什么?通过Vue的内部定义,可以知道它是一个组件。但是一个组件内部又包裹一个组件,这个包裹的节点是什么呢?
renderChildren的概念
可以肯定的是,这个内部的组件不是<keep-alive>组件 的东西,为什么呢?我们可以这样想,如果<keep-alive>需要某个元素,它直接在自己的模板里面写就可以了,没有必要在使用时嵌套一个元素。
所以这个内嵌的组件在Vue中称为renderChildren,在第8篇文章写slot时提到过,renderChildren是被挑选而不是渲染的元素,举个slot的例子容易对比清楚:
<child>
<h2 slot="title"></h2>
</child>
<child>是一个组件,它内部包裹的<h2>是准备挑选替换自己模板中的<slot>的。再看<keep-alive>元素,类比可知,它就是一个renderChildren。
抽象组件
根据原理,renderChildren是替换slot的,那么keep-alive有这个slot吗?它没有,而且什么都没有,也就是说keep-alive是一个抽象组件,它一片DOM都没有。那么接着就会出现这个问题,当渲染到它时,它怎么处理呢?下面是一段二人对话:
Vue:<keep-alive>目前要渲染到你了,快把你的Vnode拿出来。
keep-alive:我是抽象组件,什么DOM都没有,到哪里生成Vnode。
Vue:那怎么办呢?
Keep-alive:我虽然没有,但是我有一个儿子也就是renderChildren,我把它的Vnode给你,就代表渲染我了。
Vue:没问题。我们不管是渲染什么,只要你给出一个结果就可以了。
这段对话清楚的表明了这个过程,也就是说<keep-alive>组件虽然不存在DOM,但是一点都不影响它收集自己的儿子renderChildren,然后在render函数中,直接将它的Vnode返回给Vue。
keep-alive的render函数
由于keep-alive没有DOM,所以只能自定义render函数,在这个函数中,它主要做了两件事:
- 将自己的renderChildren,也就是本例中的<test-component>组件的Vnode缓存到cache中,这就是缓存的原理。
- 将该组件的Vnode返回给系统。
图示如下:

截止到这一步,Vue拿到了渲染<keep-alive>组件的Vnode,它也不管是谁的Vnode,反正有结果就可以了。
Vue渲染过程
在本例中,代码比较简单。Vue开始渲染,在解析v-if时引用了变量isShow,进而产生依赖收集,此时的实例是根实例,那么就将根实例的watcher加入。
<div id="app">
<button @click="isShow = !isShow">
修改组件
</button>
<keep-alive>
<test-component v-if="isShow"></test-component>
</keep-alive>
</div>
接着就是常规的组件渲染,过程如下:
渲染到<keep-alive>组件时,生成组件实例并挂载,在挂载过程中使用了<keep-alive>的render函数,从该函数中拿到renderChildren,也即<test-component>的Vnode,接着在patch中再创建组件,走了一遍组件创建的过程,这些是常规过程,在这里省略。
button的点击事件
button点击之后,isShow变为false,因此引起vm根实例进行全局更新,在render函数的重新计算中<test-component>不存在了,因此返回一个emptyVnode,也就是一个空Vnode。
Vue计算完成Vnode之后,再次进行patch,当前的情形如下:

Vue内部进行diff比较算法,可以看出新旧基本一样,在进行第二个Vnode,也就是<keep-alive>的Vnode比较时,调用patchNode方法。
在这个方法中,有下面一段代码:
.....
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
.....
意思是:如果存在prepatch的钩子方法,可以先执行一下。在这个方法中又调用function updateChildComponent,在该方法中主要做了下面的判断:
var needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
);
是什么意思呢?它主要说了一点,就是如果组件的肚子里面包裹有renderChildren,那么就需要强制更新一下。举个例子,列如:
<child>
<h2>..</h2>
</child>
分析:chlid组件包含一个renderChildren,那么全局渲染时,就会渲染到该组件,此时就要求<child>必须更新。我觉得它是这么思考的,由于它里面包含一个可能变化的量(一般都是用于替换slot),所以它代表一种不稳定,那么这个renderChildren只要存在就强制更新。
这一点是本篇文章的核心,它解释了一个重大问题:为什么v-if为false且全局更新之后,<test-component>就不见了,我们可以看到,这是一种比较特殊的情况,专门为renderChildren定制的强制更新!
投入异步更新队列
强制更新的结果就是将当前实例,即<keep-alive>组件实例的R-watcher投入异步更新队列。那么就会再次执行<keep-alive>的render函数,但由于v-if条件为false,此时返回一个空Vnode,最后进入patch函数。
旧Vnode是一个组件实例,新Vnod是一个空Vnode,先将新元素插入,由于是空Vnode等于没做。接着删除旧Vnode。

旧Vnode下架
removeVnodes
↓
removeAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);
旧Vnode下架就是指将<test-component>对应的DOM卸载。
removeAndInvokeRemoveHook(ch)方法主要做的事情就是将DOM元素删除,而invokeDestroyHook则做最后的善后清理工作。
invokeDestroyHook:最后的处理工作
这个方法主要调用了组件的destory钩子函数,代码如下:
destroy: function destroy (vnode) {
var componentInstance = vnode.componentInstance;
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy();
} else {
deactivateChildComponent(componentInstance, true /* direct */);
}
}
}
分析:由于它是组件而且具有keepAlive性质(被keep-alive包裹),因此不是直接销毁组件,而是卸载组件,那么就执行下面的deactivateChildComponent方法。
deactivateChildComponent方法
这个方法也做了一些事情,主要就是执行deactivated钩子方法,以此说明组件被卸载了(不是销毁了)。
重新显示时
重新显示时,全局进行渲染,此时<keep-alive>又有renderChildren,进行转化为$slot,那么就从缓存中直接取出Vnode,由于Vnode还绑定了组件实例,因此该组件的整个状态包括数据都存在。
流程结束!
总结
这篇文章写了keep-alive的整个流程,它涉及到Vue大部分知识点,四个字:波澜壮阔!很少有JS代码像这几个前端框架写的这么复杂和晦涩,我觉得主要是它的架构超级庞大,而且太多细节要处理的缘由!!
下面做个简洁的总结:
- keep-alive不存在DOM,而是将自己的renderChildren的Vnode拿出来渲染。
- keep-alive会将renderChildren的Vnode进行缓存,而且Vnode绑定组件实例,所以数据和状态齐全。
- 如果组件嵌套有renderChildren,不管怎样,它必定会重新渲染,主要是思考这个东西易变化。
- 组件下架时,不是进行销毁,而是进行拆卸。由于它是在keep-alive内部包裹的。
完整的调试代码
<div id="app">
<button @click="isShow = !isShow">
{{isShow ? '隐藏组件' : '显示组件'}}
</button>
<!-- 查看状态 -->
<div style="color:red">{{info}}</div>
<keep-alive>
<!-- 绑定两个hook,从父组件的角度监控这个组件 -->
<test-component v-if="isShow" @hook:activated="activated" @hook:deactivated="deactivated"></test-component>
</keep-alive>
</div>
<script src="../vue.js"></script>
<script>
Vue.component('testComponent', {
template: `
<div>
<p>我是测试组件</p>
<input placeholder="输入内容后隐藏再显示试试" />
</div>
`
});
new Vue({
el: '#app',
data() {
return {
isShow: true, // 控制组件显示/隐藏的状态
info: '',
}
},
methods: {
activated() {
this.info = '组件激活了!!'
},
deactivated() {
this.info = '组件卸载了!!'
}
}
});
</script>