13.7 生命周期
我们通过例子来观察keep-alive
生命周期和普通组件的不同。
在我们从child1
切换到child2
,再切回child1
过程中,chil1
不会再执行mounted
钩子,只会执行activated
钩子,而child2
也不会执行destoryed
钩子,只会执行deactivated
钩子,这是为什么?child2
的deactivated
钩子又要比child1
的activated
提前执行,这又是为什么?
13.7.1 deactivated
我们先从组件的销毁开始说起,当child1
切换到child2
时,child1
会执行deactivated
钩子而不是destoryed
钩子,这是为什么?前面分析patch
过程会对新旧节点的改变进行对比,从而尽可能范围小的去操作真实节点,当完成diff
算法并对节点操作完毕后,接下来还有一个重要的步骤是对旧的组件执行销毁移除操作。这一步的代码如下:
function patch(···) {
// 分析过的patchVnode过程
// 销毁旧节点
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
// startIdx,endIdx都为0
for (; startIdx <= endIdx; ++startIdx) {
// ch 会拿到需要销毁的组件
var ch = vnodes[startIdx];
if (isDef(ch)) {
if (isDef(ch.tag)) {
// 真实节点的移除操作
removeAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);
} else { // Text node
removeNode(ch.elm);
}
}
}
}
removeAndInvokeRemoveHook
会对旧的节点进行移除操作,其中关键的一步是会将真实节点从父元素中删除,有兴趣可以自行查看这部分逻辑。invokeDestroyHook
是执行销毁组件钩子的核心。如果该组件下存在子组件,会递归去调用invokeDestroyHook
执行销毁操作。销毁过程会执行组件内部的destory
钩子。
function invokeDestroyHook (vnode) {
var i, j;
var data = vnode.data;
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
// 执行组件内部destroy钩子
for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }
}
// 如果组件存在子组件,则遍历子组件去递归调用invokeDestoryHook执行钩子
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j]);
}
}
}
组件内部钩子前面已经介绍了init
和prepatch
钩子,而destroy
钩子的逻辑更加简单。
var componentVNodeHooks = {
destroy: function destroy (vnode) {
// 组件实例
var componentInstance = vnode.componentInstance;
// 如果实例还未被销毁
if (!componentInstance._isDestroyed) {
// 不是keep-alive组件则执行销毁操作
if (!vnode.data.keepAlive) {
componentInstance.$destroy();
} else {
// 如果是已经缓存的组件
deactivateChildComponent(componentInstance, true /* direct */);
}
}
}
}
当组件是keep-alive
缓存过的组件,即已经用keepAlive
标记过,则不会执行实例的销毁,即componentInstance.$destroy()
的过程。$destroy
过程会做一系列的组件销毁操作,其中的beforeDestroy,destoryed
钩子也是在$destory
过程中调用,而deactivateChildComponent
的处理过程却完全不同。
function deactivateChildComponent (vm, direct) {
if (direct) {
//
vm._directInactive = true;
if (isInInactiveTree(vm)) {
return
}
}
if (!vm._inactive) {
// 已经被停用
vm._inactive = true;
// 对子组件同样会执行停用处理
for (var i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i]);
}
// 最终调用deactivated钩子
callHook(vm, 'deactivated');
}
}
_directInactive
是用来标记这个被打上停用标签的组件是否是最顶层的组件。而_inactive
是停用的标志,同样的子组件也需要递归去调用deactivateChildComponent
,打上停用的标记。最终会执行用户定义的deactivated
钩子。
13.7.2 activated
现在回过头看看activated
的执行时机,同样是patch
过程,在对旧节点移除并执行销毁或者停用的钩子后,对新节点也会执行相应的钩子。这也是停用的钩子比启用的钩子先执行的原因。
function patch(···) {
// patchVnode过程
// 销毁旧节点
{
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
// 执行组件内部的insert钩子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
}
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// 当节点已经被插入时,会延迟执行insert钩子
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue;
} else {
for (var i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]);
}
}
}
同样的组件内部的insert
钩子逻辑如下:
// 组件内部自带钩子
var componentVNodeHooks = {
insert: function insert (vnode) {
var context = vnode.context;
var componentInstance = vnode.componentInstance;
// 实例已经被挂载
if (!componentInstance._isMounted) {
componentInstance._isMounted = true;
callHook(componentInstance, 'mounted');
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance);
} else {
activateChildComponent(componentInstance, true /* direct */);
}
}
},
}
当第一次实例化组件时,由于实例的_isMounted
不存在,所以会调用mounted
钩子,当我们从child2
再次切回child1
时,由于child1
只是被停用而没有被销毁,所以不会再调用mounted
钩子,此时会执行activateChildComponent
函数对组件的状态进行处理。有了分析deactivateChildComponent
的基础,activateChildComponent
的逻辑也很好理解,同样的_inactive
标记为已启用,并且对子组件递归调用activateChildComponent
做状态处理。
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 (var i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i]);
}
callHook(vm, 'activated');
}
}