Vueのマウントされたライフサイクルフックについて話しましょう

注:この記事を読むには、Vueのパッチプロセスを明確に理解する必要があります。パッチプロセスがわからない場合は、この記事を読む前にこのプロセスを理解することをお勧めします。理解しないと、混乱する可能性があります。

話をする前に、シーンを見てみましょう

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

上に示したように、App.vueファイルには2つのサブコンポーネントがあり、これらは互いに兄弟です。

コンポーネントには、作成済みとマウント済みの2つのライフサイクルフックがあります。つまり、コンポーネント名Cはcreatedの略語です。

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

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

印刷注文とは何ですか?読者は最初に決心し、後で正しいかどうかを確認できます。

vueパッチプロセスに精通している場合は、順序がaC→aM→bC→BMであると考えるかもしれません。つまり、コンポーネントが最初に作成され、次にマウントされ、次に上記のプロセスがbコンポーネントに対して繰り返されます。パッチメソッドから、コンポーネントの作成後、親コンテナに挿入するプロセスは同期プロセスであることがわかるため、このプロセスが完了した後にのみ、bコンポーネントがトラバースされ、bコンポーネントのレンダリングプロセスが実行されます。フォローされます。

実際、ブラウザによって出力される順序は、aC→bC→aM→bMです。つまり、最初に作成された2つが実行され、次にマウントされたものが実行されます。これは、上記の分析とは逆です。最初に理由を説明しましょう。子コンポーネントの作成から親コンテナへの挿入までのプロセスは同期していますが、親コンテナに挿入した後は、子コンポーネントがマウントされ、マウントされたライフが理解できます。サイクルフックはすぐには呼び出されません。ソースコードの観点から分析してみましょう。

まず、サブコンポーネントのレンダリングプロセスを確認しましょう。パッチ関数は、createElmを呼び出して実際の要素を作成します。createElmでは、createComponentを使用して、現在のvnodeがコンポーネントvnodeであるかどうかを判断し、そうである場合は、コンポーネントレンダリングプロセスに入ります。


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

复制代码

createComponentでは、コンポーネントに対応するelが親ノードに挿入され、最後にパッチ呼び出しスタックに戻って呼び出します

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

子コンポーネントにはvnode.parentがあるため、ブランチを取りますが、2番目のブランチによって呼び出されるインサートが何であるかも見てみましょう。

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]);
      }
    }
  }
复制代码

この挿入はvnode.data.hookにハングアップしています。コンポーネントの作成プロセス中に、createComponentメソッドが呼び出されます。

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。

おすすめ

転載: juejin.im/post/7077935145973448717