Let's talk about Vue's mounted life cycle hook

Note: Reading this article requires a clear understanding of Vue's patch process. If you don't know the patch process, it is recommended to understand this process before reading this article, otherwise you may feel confused.

Before we talk, let's look at a scene

<div id="app">
    <aC />
    <bC />
</div>
复制代码

As shown above, there are two subcomponents in the App.vue file, which are siblings of each other

There are two life cycle hooks in the component, created and mounted, a means the component name C is the abbreviation of created

// a组件
created() {
    console.log('aC')
  },
mounted() {
  debugger
  console.log('aM')
},

// b组件
created() {
    console.log('bC')
  },
mounted() {
  debugger
  console.log('bM')
},
复制代码

What is the print order? Readers can make up their minds first and see if they are correct later.

If you are familiar with the vue patch process, you may think that the order is aC→aM→bC→BM, that is, the a component is created first, then mounted, and then the above process is repeated for the b component. Because it can be known from the patch method that after the component is created, the process of inserting into the parent container is a synchronous process. Only after this process is completed, will the b component be traversed and the rendering process of the b component will be followed.

In fact, the order printed by the browser is aC→bC→aM→bM, that is, the two created ones are executed first, and then the mounted ones are executed, which is contrary to the above analysis. Let’s talk about the reason first. The process of the child component from being created to inserting into the parent container is still synchronous, but after inserting into the parent container, it can also be understood that the child component is mounted, and the mounted life cycle hook is not called immediately. Let's analyze it from the perspective of source code:

Let’s first review the rendering process of sub-components. The patch function calls createElm to create a real element. In createElm, createComponent is used to determine whether the current vnode is a component vnode, and if so, it enters the component rendering process.


function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
      var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */);
      }
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue);
				// 最终组件创建完后会走到这里 把组件对应的el插入到父节点
        insert(parentElm, vnode.elm, refElm);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
      }
    }
  }

复制代码

In createComponent, the el corresponding to the component is inserted into the parent node, and finally it will return to the patch call stack, calling

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
复制代码

Because the child component has vnode.parent, it will take a branch, but let's also see what the insert called by the second branch is

function invokeInsertHook (vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    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]);
      }
    }
  }
复制代码

This insert is hung on vnode.data.hook. During the component creation process, there is a call in the createComponent method

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

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 */);
      }
    }
  },
复制代码

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

function invokeInsertHook (vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    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]);
      }
    }
  }

// 在patch 开始时候 定义了insertedVnodeQueue为一个空数组
var insertedVnodeQueue = [];
复制代码

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

function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
      vnode.data.pendingInsert = null;
    }
    vnode.elm = vnode.componentInstance.$el;
    if (isPatchable(vnode)) {
			// ⚠️注意这个方法 
      invokeCreateHooks(vnode, insertedVnodeQueue);
      setScope(vnode);
    } else {
      // empty component root.
      // skip all element-related modules except for ref (#3455)
      registerRef(vnode);
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode);
    }
  }
复制代码

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了进去。

function invokeCreateHooks (vnode, insertedVnodeQueue) {
    for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
      cbs.create[i$1](emptyNode, vnode);
    }
    i = vnode.data.hook; // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) { i.create(emptyNode, vnode); }
	     // 把当前vnode push 到了insertedVnodeQueue
      if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
    }
  }
复制代码

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

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

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

Guess you like

Origin juejin.im/post/7077935145973448717