讀讀 Vue 的原始碼:生命週期

TL;DR

在 Vue 裡面的:

beforeCreate => created => beforeMount => mounted => beforeUpdate => updated => beforeDestroy => destroyed

等於:

// From: src/core/index.js
Vue => initMixin => $mount => mountComponent => Watcher => $destroy

而分隔這些階段的關鍵(也就是 =>)是 callHook 函式。

序言

這幾天想知道原生 JavaScript 的生命週期到底是怎麼搞的,所以我立刻想到讀 Vue 的原始碼,來看看 Vue 到底是用什麼原生 API 呼叫。

但我最後發現 Vue 的生命週期與完全原生 JavaScript 完全不一樣,不過看了原始碼也收穫頗豐啦。

複習一下生命週期

Vue 的生命週期有四個階段。加上階段的前與後,總共有八個階段:

before after
create beforeCreate created
mount beforeMount mounted
update beforeUpdate updated
destroy beforeDestroy destroyed

在載入必要的生命週期與事件後,四個生命週期的發動時機分別是:

Vue 官方繪製的生命週期圖

啟程:Instance 的五行函式

先從 src/core/index.js 開始。這個檔案會帶我們進到 src/core/instance/index.js 檔案。跳進去以後,你會發現 Vue 執行了五個函式:

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

比較重要的就 lifecycleMixininitMixin。這兩個以外的函式,都是初始化各種變數。你可能覺得我應該會先從 lifecycleMixin 開始講——但我要先講的是 initMixin 這個函式:從這裡接入,才能知道 Vue 的生命旅程。

開始吧。

揭露真實的自我:initMixin 做了啥

initMixin 這函式大約六十行左右。裡面有不少是針對環境不同所給出的措施。所以你看連伊斯坦堡都不鳥它了。

重點是後面的程式碼:

// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

這段程式把整個過程都解釋得很清楚:在一切開始之前,先執行 initLifecycle initEvents initRender 這三個函式。initLifecycle 很簡單,就是啟動組件的變數:

// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

雖然我不懂為什麼這函式會叫「初始化生命週期」。

接著 initEvents 會讓組件開始添加監聽器。監聽器的內部運作……頗複雜的,所以暫時不看了。在 src/core/instance/events.js 還不夠、還需要載入 src/core/vdom/helpers/update-listeners.js 啥的……

initRender 會透過 Virtual DOM 來建立組件的元素、同時藉由 defineReactive 開始載入 Vue 的響應式功能:

// src/core/instance/render.js
/* 略 */
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
/* 略 */
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
/* 略 */
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
    !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
  }, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
    !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
  }, true)
} else {
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}

附帶一題,Vue 的響應式功能可以從 src/core/observer/index.js 看到,裡面是由 getOwnPropertyDescriptordefineProperty 完成的。

完成後,就開始呼叫 callHook 了。callHook 會呼叫對應的事件監聽器。所以這就是為什麼在 Vue 裡面寫了 beforeCreate 就能執行裡面的程式。

然後從 callHook(vm, 'beforeCreate')callHook(vm, 'created') 之間又執行了 initInjections initState initProvide 三個函式。

  1. 位於 src/core/instance/inject.js 的 initInjections 其實也是去呼叫前面提到的 initRender
  2. 位於 src/core/instance/state.jsinitState 則會初始化所有有寫到的 instance 像是 props, methods, data 之類的。不過 computed 不會初始化。
  3. 位於 src/core/instance/inject.jsinitProvide 則與 inject API 有關。不過我沒用過所以不清楚。

然後 $mount 函式會把我們帶到下個階段。

直到現在才把 create 階段講完。不好了不好了。不過現在已經發現一個新的函式:callHook 了。我們可以用這函式當各階段的分界點。

風塵僕僕的旅途:mountComponent

那麼 $mount 又會去哪裡?我只好用搜尋的,搜看看 $mount 去哪裡了。按照平台不同,也所差異。但最後都會指向一個稱作 mountComponent 的函式。這函式又位於 src/core/instance/lifecycle.js 那邊。

欸……src/core/instance/lifecycle.js?那我們不就回到了生命週期嗎?好欸!快看看 mountComponent 做了啥吧!

mountComponent 主要做兩件事:把渲染的組件更新到 Virtual DOM 裡面、還有添加 update 階段所需的 Watcher 類別。你看裡面都有 callHook(vm, 'beforeUpdate') 了。

為什麼要加在這裡?

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined

看起來與 $forceUpdate 有關啊……但這也就是說,你如果要更新,就只要更新裡面的變數/狀態就好。

峰迴路轉:Watcher 怎麼這麼多方法

Watcher 這個類別在 src/core/observer/watcher.js 那邊。裡面有很多方法。但其中最重要的方法,就是 update

callHook(vm, 'updated') 又跑去哪裡了呢?

  1. update 方法會呼叫 src/core/observer/scheduler.js 裡面的 queueWatcher 函式
  2. queueWatcher 又會呼叫 flushSchedulerQueue
  3. flushSchedulerQueue 則在一段優化與更新後呼叫 callUpdatedHooks
  4. callUpdatedHooks 在最後終於呼叫了 callHook(vm, 'updated')

另外 Watcher 那段,我實際上的追蹤流程,整個是反過來的。

我看到有個組件落在地,看它的生命似已到盡頭:$destroy 怎麼跑的

那麼 callHook(vm, 'beforeDestroy') 又在哪裡? 在 src/core/instance/lifecycle.jslifecycleMixin 那裡。

lifecycleMixin 註冊了一個稱作 $destroyprototype$destroy 裡面瑣碎的程式碼太多,就用註解來幫我講好了:

// src/core/instance/lifecycle.js
// 省略
callHook(vm, 'beforeDestroy')
// remove self from parent
// teardown watchers
// remove reference from data ob
// frozen object may not have observer.
// call the last hook...
// invoke destroy hooks on current rendered tree
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
// remove __vue__ reference
// release circular reference (#6759)

這樣,整個組件的歷程就結束了。文章也接近尾聲。

結語

雖然我到現在都還不知道原生 JavaScript 的歷程。但是現在,我們已經對 Vue 的生命週期,已經有更深理解了。

可是我好累……