淺析vue中的生命週期勾點mounted

2022-03-24 13:00:35
本篇文章帶大家瞭解一下中的生命週期勾點,介紹一下vue中的mounted勾點,希望對大家有所幫助!

注:閱讀本文需要對vue的patch流程有較清晰的理解,如果不清楚patch流程,建議先了解清楚這個流程再閱讀本文,否則可能會感覺雲裡霧裡。【相關推薦:】

聊之前我們先看一個場景

<div id="app">
    <aC />
    <bC />
</div>

如上所示,App.vue檔案裡面有兩個子元件,互為兄弟關係

元件裡面自各有created和mounted兩個生命週期勾點,a表示元件名 C是created的縮寫

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

// b元件
created() {
    console.log('bC')
  },
mounted() {
  debugger
  console.log('bM')
},

請問列印順序是什麼?各位讀者可以先腦補一下,後面看看對不對。

如果對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,是則進入元件渲染流程

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插入到父節點,最後會返回到patch呼叫棧,呼叫

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);

因為子元件有vnode.parent所以會走一個分支,但是我們也看看第二個分支呼叫的insert是什麼

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]);
      }
    }
  }

這個insert是掛在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是什麼內容。

1.png

至於為什麼vue要這樣設計,是因為掛載是先子後父的,子元件插入到了父節點,但是父節點還沒有真正插入到頁面中,如果這時候立馬呼叫子元件的mounted,對框架使用者來說可能會造成困惑,因為子元件呼叫mounted的時候並沒有真正渲染到頁面中,而且此時也肯定也無法通過document.querySelector的方式操作dom。

(學習視訊分享:、)

以上就是淺析vue中的生命週期勾點mounted的詳細內容,更多請關注TW511.COM其它相關文章!