浅析vue中的生命周期钩子mounted

 3762

本篇文章带大家了解一下vue中的生命周期钩子,介绍一下vue中的mounted钩子,希望对大家有所帮助!


浅析vue中的生命周期钩子mounted


注:阅读本文需要对vue的patch流程有较清晰的理解,如果不清楚patch流程,建议先了解清楚这个流程再阅读本文,否则可能会感觉云里雾里。

聊之前我们先看一个场景

  1. <div id="app">
  2.     <aC />
  3.     <bC />
  4. </div>

如上所示,App.vue文件里面有两个子组件,互为兄弟关系

组件里面自各有created和mounted两个生命周期钩子,a表示组件名 C是created的缩写

  1. // a组件
  2. created() {
  3.     console.log('aC')
  4.   },
  5. mounted() {
  6.   debugger
  7.   console.log('aM')
  8. },
  9.  
  10. // b组件
  11. created() {
  12.     console.log('bC')
  13.   },
  14. mounted() {
  15.   debugger
  16.   console.log('bM')
  17. },

请问打印顺序是什么?各位读者可以先脑补一下,后面看看对不对。

如果对vue patch流程比较熟悉的读者,可能会认为顺序是aC→aM→bC→BM,也就是a组件先创建,再挂载,然后到b组件重复以上流程。因为从patch的方法里面可以知道,组件created后,再走到insert进父容器的过程,是一个同步的流程,只有这个流程走完后,才会遍历到b组件,走b组件的渲染流程。

实际上浏览器打印出来的顺序是aC→bC→aM→bM,也就是两个created先执行,才到mounted执行,和上面的分析相悖。这里先说原因,子组件从created到insert进父容器的过程还是同步的,但是insert进父容器后,也可以理解为子组件mounted,并没有马上调用mounted生命周期钩子。下面从源码角度分析一下:

先大概回顾一下子组件渲染流程,patch函数调用createElm创建真实element,createElm里面通过createComponent判断当前vnode是否组件vnode,是则进入组件渲染流程

  1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  2.   var i = vnode.data;
  3.   if (isDef(i)) {
  4.     var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
  5.     if (isDef(= i.hook) && isDef(= i.init)) {
  6.       i(vnode, false /* hydrating */);
  7.     }
  8.     if (isDef(vnode.componentInstance)) {
  9.       initComponent(vnode, insertedVnodeQueue);
  10.               // 最终组件创建完后会走到这里 把组件对应的el插入到父节点
  11.       insert(parentElm, vnode.elm, refElm);
  12.       if (isTrue(isReactivated)) {
  13.         reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
  14.       }
  15.       return true
  16.     }
  17.   }
  18. }

createComponent里面就把组件对应的el插入到父节点,最后会返回到patch调用栈,调用

  1. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);

因为子组件有vnode.parent所以会走一个分支,但是我们也看看第二个分支调用的insert是什么

  1. function invokeInsertHook (vnode, queue, initial) {
  2.   // delay insert hooks for component root nodes, invoke them after the
  3.   // element is really inserted
  4.   if (isTrue(initial) && isDef(vnode.parent)) {
  5.     vnode.parent.data.pendingInsert = queue;
  6.   } else {
  7.     for (var i = 0; i < queue.length; ++i) {
  8.       queue[i].data.hook.insert(queue[i]);
  9.     }
  10.   }
  11. }

这个insert是挂在vnode.data.hook上,在组件创建过程中,createComponent方法里面有一个调用

installComponentHooks,在这里把insert钩子注入了。这个方法实际定义在componentVNodeHooks对象里面,可以看到这个insert里面调用了callHook(componentInstance, 'mounted'),这里实际上就是调用子组件的mounted生命周期。

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

再来看看这个方法,子组件走第一个分支,仅仅执行了一行代码vnode.parent.data.pendingInsert = queue , 这个queue实际是在patch 开始时候,定义的insertedVnodeQueue。这里的逻辑就是把当前的insertedVnodeQueue,挂在parent的vnode data的pendingInsert上。

  1. function invokeInsertHook (vnode, queue, initial) {
  2.   // delay insert hooks for component root nodes, invoke them after the
  3.   // element is really inserted
  4.   if (isTrue(initial) && isDef(vnode.parent)) {
  5.     vnode.parent.data.pendingInsert = queue;
  6.   } else {
  7.     for (var i = 0; i < queue.length; ++i) {
  8.       queue[i].data.hook.insert(queue[i]);
  9.     }
  10.   }
  11. }
  12. // 在patch 开始时候 定义了insertedVnodeQueue为一个空数组
  13. var insertedVnodeQueue = [];

源码里面再搜索insertedVnodeQueue ,可以看到有这样一段逻辑,initComponent还是在createComponent里面调用的

  1. function initComponent (vnode, insertedVnodeQueue) {
  2.   if (isDef(vnode.data.pendingInsert)) {
  3.     insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
  4.     vnode.data.pendingInsert = null;
  5.   }
  6.   vnode.elm = vnode.componentInstance.$el;
  7.   if (isPatchable(vnode)) {
  8.           // ⚠️注意这个方法 
  9.     invokeCreateHooks(vnode, insertedVnodeQueue);
  10.     setScope(vnode);
  11.   } else {
  12.     // empty component root.
  13.     // skip all element-related modules except for ref (#3455)
  14.     registerRef(vnode);
  15.     // make sure to invoke the insert hook
  16.     insertedVnodeQueue.push(vnode);
  17.   }
  18. }

insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) 重点看这一行代码,把 vnode.data.pendingInsert这个数组每一项push到当前vnode的insertedVnodeQueue中,注意这里是通过apply的方式,所以是把 vnode.data.pendingInsert这个数组每一项都push,而不是push pendingInsert这个列表进去。也就是说在这里,组件把他的子组件的insertedVnodeQueue里面的item收集了,因为渲染是一个深度递归的过程,所有最后根组件的insertedVnodeQueue能拿到所有子组件的insertedVnodeQueue里面的每一项。

从invokeInsertHook的queue[i].data.hook.insert(queue[i]) 这一行可以看出,insertedVnodeQueue里面的item应该是vnode。源码中搜索insertedVnodeQueue.push ,可以发现是invokeCreateHooks这个方法把当前vnode push了进去。

  1. function invokeCreateHooks (vnode, insertedVnodeQueue) {
  2.   for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
  3.     cbs.create[i$1](emptyNode, vnode);
  4.   }
  5.   i = vnode.data.hook; // Reuse variable
  6.   if (isDef(i)) {
  7.     if (isDef(i.create)) { i.create(emptyNode, vnode); }
  8.        // 把当前vnode push 到了insertedVnodeQueue
  9.     if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
  10.   }
  11. }

对于组件vnode来说,这个方法还是在initComponent中调用的。

到这里就很清晰,子组件insert进父节点后,并不会马上调用mounted钩子,而是把组件对应到vnode插入到父vnode的insertedVnodeQueue中,层层递归,最终根组件拿到所有子组件的vnode,再依次循环遍历,调用vnode的insert钩子,从而调用了mounted钩子。这里是先进先出的,第一个被push进去的第一个被拿出来调用,所以最深的那个子组件的mounted先执行。最后附上一张源码调试的图,可以清晰的看到根组件的insertedVnodeQueue是什么内容。


浅析vue中的生命周期钩子mounted


至于为什么vue要这样设计,是因为挂载是先子后父的,子组件插入到了父节点,但是父节点还没有真正插入到页面中,如果这时候立马调用子组件的mounted,对框架使用者来说可能会造成困惑,因为子组件调用mounted的时候并没有真正渲染到页面中,而且此时也肯定也无法通过document.querySelector的方式操作dom。


本文网址:https://www.zztuku.com/detail-11356.html
站长图库 - 浅析vue中的生命周期钩子mounted
申明:本文转载于《掘金社区》,如有侵犯,请 联系我们 删除。

评论(0)条

您还没有登录,请 登录 后发表评论!

提示:请勿发布广告垃圾评论,否则封号处理!!

    编辑推荐