面試官:Vue 的生命週期之間到底做了什麼事清?(原始碼詳解)

晨曦時夢見兮發表於2020-04-04

前言

相信大家對 Vue 有哪些生命週期早就已經爛熟於心,但是對於這些生命週期的前後分別做了哪些事情,可能還有些不熟悉。

本篇文章就從一個完整的流程開始,詳細講解各個生命週期之間發生了什麼事情。

注意本文不涉及 keep-alive 的場景和錯誤處理的場景。

初始化流程

new Vue

new Vue(options) 開始作為入口,Vue 只是一個簡單的建構函式,內部是這樣的:

function Vue (options) {
  this._init(options)
}
複製程式碼

進入了 _init 函式之後,先初始化了一些屬性,然後開始第一個生命週期:

callHook(vm, 'beforeCreate')
複製程式碼

beforeCreate被呼叫完成

beforeCreate 之後

  1. 初始化 inject
  2. 初始化 state
    • 初始化 props
    • 初始化 methods
    • 初始化 data
    • 初始化 computed
    • 初始化 watch
  3. 初始化 provide

所以在 data 中可以使用 props 上的值,反過來則不行。

然後進入 created 階段:

callHook(vm, 'created')
複製程式碼

created被呼叫完成

呼叫 $mount 方法,開始掛載元件到 dom 上。

如果使用了 runtime-with-compile 版本,則會把你傳入的 template 選項,或者 html 文字,通過一系列的編譯生成 render 函式。

  • 編譯這個 template,生成 ast 抽象語法樹。
  • 優化這個 ast,標記靜態節點。(渲染過程中不會變的那些節點,優化效能)。
  • 根據 ast,生成 render 函式。

對應具體的程式碼就是:

const ast = parse(template.trim(), options)
if (options.optimize !== false) {
  optimize(ast, options)
}
const code = generate(ast, options)
複製程式碼

如果是腳手架搭建的專案的話,這一步 vue-cli 已經幫你做好了,所以就直接進入 mountComponent 函式。

那麼,確保有了 render 函式後,我們就可以往渲染的步驟繼續進行了

beforeMount被呼叫完成

渲染元件的函式 定義好,具體程式碼是:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
複製程式碼

拆解來看,vm._render 其實就是呼叫我們上一步拿到的 render 函式生成一個 vnode,而 vm._update 方法則會對這個 vnode 進行 patch 操作,幫我們把 vnode 通過 createElm函式建立新節點並且渲染到 dom節點 中。

接下來就是執行這段程式碼了,是由 響應式原理 的一個核心類 Watcher 負責執行這個函式,為什麼要它來代理執行呢?因為我們需要在這段過程中去 觀察 這個函式讀取了哪些響應式資料,將來這些響應式資料更新的時候,我們需要重新執行 updateComponent 函式。

如果是更新後呼叫 updateComponent 函式的話,updateComponent 內部的 patch 就不再是初始化時候的建立節點,而是對新舊 vnode 進行 diff,最小化的更新到 dom節點 上去。具體過程可以看我的上一篇文章:

為什麼 Vue 中不要用 index 作為 key?(diff 演算法詳解)

這一切交給 Watcher 完成:

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
複製程式碼

注意這裡在before 屬性上定義了beforeUpdate 函式,也就是說在 Watcher 被響應式屬性的更新觸發之後,重新渲染新檢視之前,會先呼叫 beforeUpdate 生命週期。

關於 Watcher 和響應式的概念,如果你還不清楚的話,可以閱讀我之前的文章:

手把手帶你實現一個最精簡的響應式系統來學習Vue的data、computed、watch原始碼

注意,在 render 的過程中,如果遇到了 子元件,則會呼叫 createComponent 函式。

createComponent 函式內部,會為子元件生成一個屬於自己的建構函式,可以理解為子元件自己的 Vue 函式:

Ctor = baseCtor.extend(Ctor)
複製程式碼

在普通的場景下,其實這就是 Vue.extend 生成的建構函式,它繼承自 Vue 函式,擁有它的很多全域性屬性。

這裡插播一個知識點,除了元件有自己的生命週期外,其實 vnode 也是有自己的 生命週期的,只不過我們平常開發的時候是接觸不到的。

那麼子元件會有自己的 init 週期,這個週期內部會做這樣的事情:

// 建立子元件
const child = createComponentInstanceForVnode(vnode)
// 掛載到 dom 上
child.$mount(vnode.elm)
複製程式碼

createComponentInstanceForVnode 內部又做了什麼事呢?它會去呼叫 子元件 的建構函式。

new vnode.componentOptions.Ctor(options)
複製程式碼

建構函式的內部是這樣的:

const Sub = function VueComponent (options) {
  this._init(options)
}
複製程式碼

這個 _init 其實就是我們文章開頭的那個函式,也就是說,如果遇到 子元件,那麼就會優先開始子元件的構建過程,也就是說,從 beforeCreated 重新開始。這是一個遞迴的構建過程。

也就是說,如果我們有 父 -> 子 -> 孫 這三個元件,那麼它們的初始化生命週期順序是這樣的:

父 beforeCreate 
父 create 
父 beforeMount 
子 beforeCreate 
子 create 
子 beforeMount 
孫 beforeCreate 
孫 create 
孫 beforeMount 
孫 mounted 
子 mounted 
父 mounted 
複製程式碼

然後,mounted 生命週期被觸發。

mounted被呼叫完成

到此為止,元件的掛載就完成了,初始化的生命週期結束。

更新流程

當一個響應式屬性被更新後,觸發了 Watcher 的回撥函式,也就是 vm._update(vm._render()),在更新之前,會先呼叫剛才在 before 屬性上定義的函式,也就是

callHook(vm, 'beforeUpdate')
複製程式碼

注意,由於 Vue 的非同步更新機制,beforeUpdate 的呼叫已經是在 nextTick 中了。 具體程式碼如下:

nextTick(flushSchedulerQueue)

function flushSchedulerQueue {
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
     // callHook(vm, 'beforeUpdate')
      watcher.before()
    }
 }
}
複製程式碼

beforeUpdate被呼叫完成

然後經歷了一系列的 patchdiff 流程後,元件重新渲染完畢,呼叫 updated 鉤子。

注意,這裡是對 watcher 倒序 updated 呼叫的。

也就是說,假如同一個屬性通過 props 分別流向 父 -> 子 -> 孫 這個路徑,那麼收集到依賴的先後也是這個順序,但是觸發 updated 鉤子確是 孫 -> 子 -> 父 這個順序去觸發的。

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}
複製程式碼

updated被呼叫完成

至此,渲染更新流程完畢。

銷燬流程

在剛剛所說的更新後的 patch 過程中,如果發現有元件在下一輪渲染中消失了,比如 v-for 對應的陣列中少了一個資料。那麼就會呼叫 removeVnodes 進入元件的銷燬流程。

removeVnodes 會呼叫 vnodedestroy 生命週期,而 destroy 內部則會呼叫我們相對比較熟悉的 vm.$destroy()。(keep-alive 包裹的子元件除外)

這時,就會呼叫 callHook(vm, 'beforeDestroy')

beforeDestroy被呼叫完成

之後就會經歷一系列的清理邏輯,清除父子關係、watcher 關閉等邏輯。但是注意,$destroy 並不會把元件從檢視上移除,如果想要手動銷燬一個元件,則需要我們自己去完成這個邏輯。

然後,呼叫最後的 callHook(vm, 'destroyed')

destroyed被呼叫完成

總結

至此為止,Vue 的生命週期我們就完整的回顧了一遍。知道各個生命週期之間發生了什麼事,可以讓我們在編寫 Vue 元件的過程中更加胸有成竹。

希望這篇文章對你有幫助。

❤️感謝大家

1.如果本文對你有幫助,就點個贊支援下吧,你的「贊」是我創作的動力。

2.關注公眾號「前端從進階到入院」即可加我好友,我拉你進「前端進階交流群」,大家一起共同交流和進步。

面試官:Vue 的生命週期之間到底做了什麼事清?(原始碼詳解)

相關文章