1. 程式人生 > >路由場景下父子元件的生命週期順序來個刨根問底

路由場景下父子元件的生命週期順序來個刨根問底

大家中秋假期快樂,假期分享一些原理設計文章給大家

原創不易,歡迎轉發,一起學習(凌晨寫的,不容易哈,收藏或者點個贊吧)


在常見的單頁應用中,我們都會有一個根 App.vue 檔案,裡面放置一個 router-view 然後配置路由來切換.

很多人在子父元件巢狀關係下的生命週期鉤子函式如何應用,誰先誰後(比如哪個用來發送請求,資料傳遞)等有所疑問。

本文聚焦 mounted 事件(需要 created 的可以留言哈),先拋結論:

子元件一層一層往外觸發,最終觸發根 App.vue 的 mounted

驗證的做法很簡單:

你只需要在每一個元件裡面的 mounted 增加列印日誌就可以看到了,我們具體來看看設計原理

現在假設我們配置了路由:

一級是 /user/:id 二級是 profile


const router = new VueRouter({
  routes: [
    { 
      path: '/user/:id', 
      component: User,
      children: [
        {
          // 當 /user/:id/profile 匹配成功,
          // UserProfile 會被渲染在 User 的 <router-view> 中
          path: 'profile',
          component: UserProfile
        }
      ]
    }
  ]
})

先看一下所有的 mounted 最終在各自元件裡面是如何被呼叫的:

通過 vm.$options 獲取元件內部的配置,然後通過 call 方法

下面的我們又會遇到 vnode(所以掌握它很重要,在前面的 vue.js 原始碼原創系列 ref 與 $refs 如何關聯 也提到了一些 vnode,感興趣可以看看),就是下面 componentVNodeHooks 裡面的 insert 函式的引數


var componentVNodeHooks = {
  insert: function insert (vnode) {}
}

裡面呢,第一步:從 vnode 裡面獲取 componentInstance


var componentInstance = vnode.componentInstance;

然後判斷 _isMounted 是否已經執行過 mounted (很常用的狀態二次確定的變數,前面的 _ 一般代表內部使用)


if (!componentInstance._isMounted) {
  componentInstance._isMounted = true;
  callHook(componentInstance, 'mounted');
}

上面我們就用到了 callHook 函數了,傳入的第二個引數也正是本文討論的生命週期的 mounted

再往後有一個 invokeInsertHook 函式


function invokeInsertHook (vnode, queue, initial) {
}

注意一下原始碼裡面的註釋:

delay insert hooks for component root nodes, invoke them after the element is really inserted

設定了 pendingInsert(後面會在 initComponent 中使用),程式碼如下:


if (isTrue(initial) && isDef(vnode.parent)) {
  vnode.parent.data.pendingInsert = queue;
}

內部設計:迴圈 queue 這個包含 vnode 的陣列物件,如圖所示:

注意一下標註的 data.hook,下面的程式碼片段會使用到,也就是呼叫上面提到的 componentVNodeHooks 物件的 insert


for (var i = 0; i < queue.length; ++i) {
  queue[i].data.hook.insert(queue[i]);
}

再往下,帶著疑問:

這個 queue 是如何生成 vnode 陣列的呢?

最開始定義一個空陣列:


var insertedVnodeQueue = [];

在剛才的 物件中還有 init


var componentVNodeHooks = {
  init: function init () {
    // ...
  }
}

init 函式內部定義一個 child


var child = vnode.componentInstance = createComponentInstanceForVnode(...)

然後會呼叫一個 $mount


child.$mount(hydrating ? vnode.elm : undefined, hydrating);

在函式 initComponent 中會用到之前的 pendingInsert,而且 insertedVnodeQueue 這個陣列物件會呼叫 push 插入元素


function initComponent (vnode, insertedVnodeQueue) {
  if (isDef(vnode.data.pendingInsert)) {
    insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
    vnode.data.pendingInsert = null;
  }
}

在函式 invokeCreateHooks 內部insertedVnodeQueue 這個陣列物件會呼叫 push 插入元素


function invokeCreateHooks (vnode, insertedVnodeQueue) {
  i = vnode.data.hook; // Reuse variable
  if (isDef(i)) {
    if (isDef(i.insert)) { 
      insertedVnodeQueue.push(vnode); 
    }
  }
}

在函式 mountComponent 內部當 vm.$vnode 為 null 也會呼叫 callHook,第二個引數傳入 mounted


function mountComponent () {
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
}

來源:https://segmentfault.com/a/1190000016499274