注:閱讀本文需要對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是什麼內容。
至於為什麼vue要這樣設計,是因為掛載是先子後父的,子元件插入到了父節點,但是父節點還沒有真正插入到頁面中,如果這時候立馬呼叫子元件的mounted,對框架使用者來說可能會造成困惑,因為子元件呼叫mounted的時候並沒有真正渲染到頁面中,而且此時也肯定也無法通過document.querySelector的方式操作dom。
(學習視訊分享:、)
以上就是淺析vue中的生命週期勾點mounted的詳細內容,更多請關注TW511.COM其它相關文章!