13.7 生命周期

我们通过例子来观察keep-alive生命周期和普通组件的不同。

13.7 生命周期 - 图1

在我们从child1切换到child2,再切回child1过程中,chil1不会再执行mounted钩子,只会执行activated钩子,而child2也不会执行destoryed钩子,只会执行deactivated钩子,这是为什么?child2deactivated钩子又要比child1activated提前执行,这又是为什么?

13.7.1 deactivated

我们先从组件的销毁开始说起,当child1切换到child2时,child1会执行deactivated钩子而不是destoryed钩子,这是为什么?前面分析patch过程会对新旧节点的改变进行对比,从而尽可能范围小的去操作真实节点,当完成diff算法并对节点操作完毕后,接下来还有一个重要的步骤是对旧的组件执行销毁移除操作。这一步的代码如下:

  1. function patch(···) {
  2. // 分析过的patchVnode过程
  3. // 销毁旧节点
  4. if (isDef(parentElm)) {
  5. removeVnodes(parentElm, [oldVnode], 0, 0);
  6. } else if (isDef(oldVnode.tag)) {
  7. invokeDestroyHook(oldVnode);
  8. }
  9. }
  10. function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
  11. // startIdx,endIdx都为0
  12. for (; startIdx <= endIdx; ++startIdx) {
  13. // ch 会拿到需要销毁的组件
  14. var ch = vnodes[startIdx];
  15. if (isDef(ch)) {
  16. if (isDef(ch.tag)) {
  17. // 真实节点的移除操作
  18. removeAndInvokeRemoveHook(ch);
  19. invokeDestroyHook(ch);
  20. } else { // Text node
  21. removeNode(ch.elm);
  22. }
  23. }
  24. }
  25. }

removeAndInvokeRemoveHook会对旧的节点进行移除操作,其中关键的一步是会将真实节点从父元素中删除,有兴趣可以自行查看这部分逻辑。invokeDestroyHook是执行销毁组件钩子的核心。如果该组件下存在子组件,会递归去调用invokeDestroyHook执行销毁操作。销毁过程会执行组件内部的destory钩子。

  1. function invokeDestroyHook (vnode) {
  2. var i, j;
  3. var data = vnode.data;
  4. if (isDef(data)) {
  5. if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
  6. // 执行组件内部destroy钩子
  7. for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }
  8. }
  9. // 如果组件存在子组件,则遍历子组件去递归调用invokeDestoryHook执行钩子
  10. if (isDef(i = vnode.children)) {
  11. for (j = 0; j < vnode.children.length; ++j) {
  12. invokeDestroyHook(vnode.children[j]);
  13. }
  14. }
  15. }

组件内部钩子前面已经介绍了initprepatch钩子,而destroy钩子的逻辑更加简单。

  1. var componentVNodeHooks = {
  2. destroy: function destroy (vnode) {
  3. // 组件实例
  4. var componentInstance = vnode.componentInstance;
  5. // 如果实例还未被销毁
  6. if (!componentInstance._isDestroyed) {
  7. // 不是keep-alive组件则执行销毁操作
  8. if (!vnode.data.keepAlive) {
  9. componentInstance.$destroy();
  10. } else {
  11. // 如果是已经缓存的组件
  12. deactivateChildComponent(componentInstance, true /* direct */);
  13. }
  14. }
  15. }
  16. }

当组件是keep-alive缓存过的组件,即已经用keepAlive标记过,则不会执行实例的销毁,即componentInstance.$destroy()的过程。$destroy过程会做一系列的组件销毁操作,其中的beforeDestroy,destoryed钩子也是在$destory过程中调用,而deactivateChildComponent的处理过程却完全不同。

  1. function deactivateChildComponent (vm, direct) {
  2. if (direct) {
  3. //
  4. vm._directInactive = true;
  5. if (isInInactiveTree(vm)) {
  6. return
  7. }
  8. }
  9. if (!vm._inactive) {
  10. // 已经被停用
  11. vm._inactive = true;
  12. // 对子组件同样会执行停用处理
  13. for (var i = 0; i < vm.$children.length; i++) {
  14. deactivateChildComponent(vm.$children[i]);
  15. }
  16. // 最终调用deactivated钩子
  17. callHook(vm, 'deactivated');
  18. }
  19. }

_directInactive是用来标记这个被打上停用标签的组件是否是最顶层的组件。而_inactive是停用的标志,同样的子组件也需要递归去调用deactivateChildComponent,打上停用的标记。最终会执行用户定义的deactivated钩子。

13.7.2 activated

现在回过头看看activated的执行时机,同样是patch过程,在对旧节点移除并执行销毁或者停用的钩子后,对新节点也会执行相应的钩子。这也是停用的钩子比启用的钩子先执行的原因。

  1. function patch(···) {
  2. // patchVnode过程
  3. // 销毁旧节点
  4. {
  5. if (isDef(parentElm)) {
  6. removeVnodes(parentElm, [oldVnode], 0, 0);
  7. } else if (isDef(oldVnode.tag)) {
  8. invokeDestroyHook(oldVnode);
  9. }
  10. }
  11. // 执行组件内部的insert钩子
  12. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  13. }
  14. function invokeInsertHook (vnode, queue, initial) {
  15. // delay insert hooks for component root nodes, invoke them after the
  16. // 当节点已经被插入时,会延迟执行insert钩子
  17. if (isTrue(initial) && isDef(vnode.parent)) {
  18. vnode.parent.data.pendingInsert = queue;
  19. } else {
  20. for (var i = 0; i < queue.length; ++i) {
  21. queue[i].data.hook.insert(queue[i]);
  22. }
  23. }
  24. }

同样的组件内部的insert钩子逻辑如下:

  1. // 组件内部自带钩子
  2. var componentVNodeHooks = {
  3. insert: function insert (vnode) {
  4. var context = vnode.context;
  5. var componentInstance = vnode.componentInstance;
  6. // 实例已经被挂载
  7. if (!componentInstance._isMounted) {
  8. componentInstance._isMounted = true;
  9. callHook(componentInstance, 'mounted');
  10. }
  11. if (vnode.data.keepAlive) {
  12. if (context._isMounted) {
  13. // vue-router#1212
  14. // During updates, a kept-alive component's child components may
  15. // change, so directly walking the tree here may call activated hooks
  16. // on incorrect children. Instead we push them into a queue which will
  17. // be processed after the whole patch process ended.
  18. queueActivatedComponent(componentInstance);
  19. } else {
  20. activateChildComponent(componentInstance, true /* direct */);
  21. }
  22. }
  23. },
  24. }

当第一次实例化组件时,由于实例的_isMounted不存在,所以会调用mounted钩子,当我们从child2再次切回child1时,由于child1只是被停用而没有被销毁,所以不会再调用mounted钩子,此时会执行activateChildComponent函数对组件的状态进行处理。有了分析deactivateChildComponent的基础,activateChildComponent的逻辑也很好理解,同样的_inactive标记为已启用,并且对子组件递归调用activateChildComponent做状态处理。

  1. function activateChildComponent (vm, direct) {
  2. if (direct) {
  3. vm._directInactive = false;
  4. if (isInInactiveTree(vm)) {
  5. return
  6. }
  7. } else if (vm._directInactive) {
  8. return
  9. }
  10. if (vm._inactive || vm._inactive === null) {
  11. vm._inactive = false;
  12. for (var i = 0; i < vm.$children.length; i++) {
  13. activateChildComponent(vm.$children[i]);
  14. }
  15. callHook(vm, 'activated');
  16. }
  17. }