• 13.7 生命周期
    • 13.7.1 deactivated
    • 13.7.2 activated

    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. }