[Vue.js進階]從源碼角度剖析Vue的生命週期

前言

使用Vue在平常開發中會頻繁接觸和使用生命週期,在官方文檔中是這麼解釋生命週期的:javascript

每一個 Vue 實例在被建立時都要通過一系列的初始化過程——例如,須要設置數據監聽、編譯模板、將實例掛載到 DOM 並在數據變化時更新 DOM 等。同時在這個過程當中也會運行一些叫作生命週期鉤子的函數,這給了用戶在不一樣階段添加本身的代碼的機會。vue

比如人的生老病死的過程,Vue一樣也有從組建初始化到組件掛載,組件更新,組件銷燬的一系列過程,而生命週期鉤子,是一個函數,可讓開發者在Vue到達某個時間段的時候作一些事情java

最多見的就是在mounted鉤子中發送ajax請求獲取當前的頁面組件所須要的數據node

可是對於Vue.js進階來講,只知道生命週期的拼寫和對應的觸發時機確定是不夠的,爲何鉤子函數不能是一個箭頭函數,爲何在data中有時候沒法獲取定義的數據,咱們經過this獲取data中的數據真的直接保存在this下了嗎,Vue又是怎麼作到無感知的事件監聽/事件解綁git

在這篇文章中,我將會帶你們深刻Vue的源碼,從源碼中分析Vue的生命週期github

文中的源碼截圖只保留核心邏輯 完整源碼地址ajax

Vue版本:2.5.21vue-router

源碼概覽

當咱們在main.js中實例化Vue的時候,會通過一些邏輯,而後進入到_init函數開始Vue的生命週期,其實從這些函數的命名方式中就能大體看出Vue是如何運行的了,接下來咱們逐個分析每一個函數具體作了什麼vuex

合併配置項

從上面的圖中能看到,在生命週期中第一件事就是合併配置項,而對於根實例和組件實例,Vue的處理方式是不一樣的(在main.js中new Vue生成的是根實例,其他所有都是組件實例),根實例傳入的options參數裏不會有_isComponent屬性,反之爲true(實例化的時機不一樣,傳入的參數也不一樣,感興趣的朋友能夠查看相關實例化的文章)後端

爲了避免必要的干涉,這裏沒有引入vue-router,vuex

根實例合併配置項

對於根實例會走false的邏輯,進入mergeOptions函數,合併Vue的各個配置項options,好比mixins,props,methods,watch,computed,生命週期鉤子等等,這是整個項目中第一次的合併配置。Vue會將全部的合併策略都保存在一個strats對象中,而後依次遍歷當前實例和parent的同一個屬性,再去starts找那個屬性對應的合併策略

經過斷點能夠看到strats保存了不少合併的策略

咱們沒有必要每一個合併策略都去看一遍,儘可能把精力放在整個流程中,不要撿了芝麻丟了西瓜。第一次的合併中,Vue會經過resolveConstructorOptions(vm.constructor)獲取Vue構造器的靜態屬性options做爲parent,這個options包含了一些預先設置好的配置項,而child就是咱們給根實例實例化的時候傳入的一些參數,對應例子中上圖的render函數

Vue預先設置的配置項做爲第一次的parent:

根實例實例化傳入的參數:

根實例的合併策略其實很簡單,主要就是把Vue框架內置的一些配置項和開發者在main.js中實例化Vue構造器傳入的參數進行一次簡單的合併,做爲根實例的$options屬性

組件實例合併配置項

組件實例合併配置項並不在_init函數中,由於組件實例和根實例不一樣,組件實例是由組件構造器實例化的,而根實例是由Vue構造器實例化的,而組件構造器又是繼承自Vue的它須要經過Vue.extend方法去繼承Vue構造函數,我畫了張圖方便理解

Vue這麼作符合面向對象的設計模式,一個組件實質上是一個構造器函數(進一步能夠認爲是一個class),這樣在一個頁面中引入多個相同的組件只須要屢次實例化組件構造器就能夠了,而且能夠作到實例之間互相獨立

而面向對象另一個好處就是能夠實現繼承,體如今Vue框架中則是將組件構造器繼承Vue構造器,從而組件構造器可以得到Vue構造器內置的一些配置項

組件實例合併配置項在src/core/global-api/extend.js,一樣會調用mergeOptions組件實例合併配置項會將Vue框架內置的配置項和當前組件配置項進行合併並賦值給組件構造器的靜態屬性options

再次回到mergeOptions中,這裏就只例舉一個生命週期的合併策略,直接貼上源碼並附上流程圖方便理解

這裏我用了父級而不是父組件,由於Vue的組件通常繼承自Vue構造函數而不是父組件,經過流程圖能夠發現,Vue會保證生命週期函數始終是一個數組,而且以父=>子的順序排列的,Vue在執行某個生命週期的時候會遍歷這個數組依次執行函數,因此當咱們在Vue構造器和組件構造器中的同一個生命週期裏都定義了生命週期函數,會先執行Vue構造器中的那個

繼承了Vue構造器後纔會實例化子組件生成組件實例,再進入到_init函數,這個時候_isComponent爲true會執行initInternalComponent,它會給組件實例建立$options屬性,指向子組件構造器的靜態屬性options,這樣就可以經過組件實例的$options屬性訪問到當前組件的配置項以及Vue框架內置的配置項(包括全局組件,全局混入)

小結

  • 生命週期中第一件事就是合併配置項,對於根實例和組件實例合併的時機不一樣
  • 根實例是在new Vue的時候進行合併,將Vue內置的配置項和new Vue傳入的配置項進行合併
  • 對於組件實例來講,先會建立子組件的構造器,而且調用Vue.extend繼承Vue構造器,繼承的時候將Vue內置的配置項和組件配置項進行合併,並將結果保存在構造器的options屬性中,以後在建立組件實例的時候進入initInternalComponent方法會將組件實例的$options指向組件構造器的options屬性
  • Vue框架會根據不一樣的配置執行不一樣的合併策略

代理開發環境的錯誤

非生產環境下會進入initProxy函數,經過ES6的Proxy給vm實例作一層攔截,主要做用是給開發環境下一些不合理的配置作出一些自定義的警告

上面的報錯不少開發者都遇到過,其實就是在這個時候經過Proxy的has攔截器,當某個屬性不在vm實例上卻被模版引用的時候,Vue會給出一些友好的提示

初始化自定義事件

隨後進入initLifecycle,這部分沒什麼好講的,初始化實例的一些生命週期的狀態和一些額外屬性,接着會進入初始化組件的自定義事件

initEvents只會掛載自定義事件,即組件中使用v-on監聽的非native的事件(原生的DOM事件並不是在initEvents中掛載)。Vue會把這些父組件中聲明的自定義的事件保存在子組件的_parentListeners屬性中(vm是子組件的組件實例,_parentListeners是在initInternalComponent中定義的)

進入updateComponentListeners,發現Vue會調用add函數註冊全部的自定義事件,而對於組件來講add函數就會調用$on來達到監聽自定義事件的效果

//https://github.com/vuejs/vue/blob/dev/src/core/instance/events.js#L24
function add (event, fn) {
  target.$on(event, fn)
}

//https://github.com/vuejs/vue/blob/dev/src/core/vdom/helpers/update-listeners.js#L83
//調用add註冊自定義事件(後面3個參數可忽略)
add(event.name, cur, event.capture, event.passive, event.params)
複製代碼

beforeCreate

添加完自定義事件後,進入initRender,定義插槽和給render函數的參數createElement,另外會將Vue的$attrs,$listeners變成響應式的屬性

接着會執行callHook(vm, 'beforeCreate'),從字面上來看就能猜出Vue在這個時候會調用beforeCreate這個生命週期函數,在以前合併配置項的時候就提到,生命週期函數最終會被包裹成一個數組,因此事實上Vue也支持這麼寫

callHook函數會根據傳入的參數拿到$options屬性中對應的生命週期函數組成的數組,這裏傳入了beforeCreate,因此會得到beforeCreate中定義的全部生命週期函數,以後順序遍歷而且用call方法給每一個生命週期函數綁定了this上下文,這就是爲何生命週期函數不能使用剪頭函數書寫的緣由

初始化數據

接着執行initInjections,這部分是用來初始化inject這個api,因爲平常開發使用頻率較少就不詳細解釋了(實際上是我懶得研究-.-)

隨後會進入另一個關鍵的函數initState,它會依次初始化props,methods,data,computed,watch,咱們一個個來說解

props

組件之間通訊的時候,父組件給子組件傳參,子組件須要定義props來接受父組件傳過來的屬性,而Vue規定,子組件是不能修改父組件傳來的props,由於這違背了單項數據流,會致使組件之間很是難以管理,若是在子組件修改了props,Vue會發出一個警告

而Vue又是怎麼知道開發者修改了props的屬性呢?緣由仍是利用了訪問器描述符setter

瞭解過響應式原理的朋友應該對這個有所熟悉,Vue會將props對象變成一個響應式對象,而且第四個參數是一個自定義的setter,當props被修改了會觸發這個setter,一單違背了單項數據流時就會報出這個警告

methods

對於methods,Vue會定義一些開發過程當中的不規範的警告,隨後會將全部的method綁定vm實例,這樣咱們就能夠直接經過this獲取當前的vm實例

data

到了最關鍵的data,data中通常保存的是當前組件須要使用的數據,除了根實例以外,組件實例的data通常都是一個函數,由於JS引用類型的特色,若是使用對象,當存在多個相同的組件,其中一個組件修改了data數據,會反映到全部的組件。當data做爲一個函數返回一個對象時,每次執行都會生成一個新的對象,能夠有效的解決這個問題

初始化data會執行initData這個函數,內部會執行定義的data函數而且把當前實例做爲this值,而且賦值給_data這個內部屬性,值得注意的是,在執行data函數的過程當中是獲取不到computed中的數據,由於computed中的數據此時還沒初始化

隨後執行proxy函數,它的做用是將vm._data的屬性映射到vm屬性上,起到了"代理"的做用,這樣作是爲了在開發過程當中直接書寫this[key]的形式,其原理依舊是利用了getter/setter,當咱們訪問this[key]的時候會觸發getter,直接指向this._data[key],setter同理

有人會問,那爲啥不直接寫在vm實例上呢?由於咱們須要將數據放在一個統一的對象上進行管理,爲的是下一步把_data經過observe變成一個響應式對象。而爲了在開發的時候書寫更加簡潔,Vue採起了這種方法,很是的討巧

computed

到了初始化computed,Vue會給每一個計算屬性生成一個computed watcher,只有當這個計算屬性的依賴項改變了纔會去通知computed watcher更新這個計算屬性,從而既能達到實時更新數據,又不會浪費性能,也是Vue很是棒的功能

watch

初始化watch的時候最終會調用$watch方法,生成一個user watcher,當監聽的屬性發生改變就會當即通知user watcher執行回調

created

再調用initProvide初始化provide後就會執行callHook(vm, 'beforeCreate'),和beforeCreate同樣,依次遍歷定義在$options上的created數組,執行生命週期函數

至此整個組件建立完畢,其實這個時候就能夠和後端進行交互獲取數據了,可是對於真正的DOM節點尚未被渲染出來,一些須要和DOM的交互操做還沒法在created鉤子中執行,即沒法在created鉤子中有操做生成視圖的DOM

掛載過程

回到_init函數,已經到了最後一行,會判斷$options是否有el屬性,在Vue-cli2的時候,cli會自動在new Vue的時候傳入el參數,而對於Vue-cli3並無這麼作,而是生成根實例後主動調用$mount並傳入了掛載的節點,其實二者都是同樣的,也可使用$mount來實現組件的手動掛載

Vue-cli2:

Vue-cli3:

$mount最終會執行mountComponent這個函數

剛剛從_init的長篇大論中逃出來,又要跳進mountComponent這個坑

組件掛載我這裏不會展開詳解,儘可能把重心放在生命週期方面,有興趣的朋友能夠自行了解,或者看我底下的連接

beforeMount

當組件執行$mount而且擁有掛載點和渲染函數的時候,就會觸發beforeMount的鉤子,準備組件的掛載

渲染視圖的函數updateComponent

以後Vue會定義一個updateComponent函數,這個函數是整個掛載的核心,它由2部分組成,_render函數和_update函數

  • render函數最終會執行以前在initRender定義的createElement函數,做用是建立vnode
  • update函數會將上面的render函數生成的vnode渲染成一個真實的DOM樹,並掛載到掛載點上

第一次執行updateComponent會渲染出整個DOM樹,這個時候頁面就完整的被展示了

渲染watcher

而後會實例化一個"渲染watcher",將updateComponent做爲回調函數傳入,內部會當即執行一次updateComponet函數

watcher顧名思義是用來觀察的,渲染watcher簡而言之,就是會觀察模版中依賴變量的是否變化來決定是否須要刷新頁面,而updateComponet就是一個用來更新頁面的函數,因此將這個函數做爲回調傳入。對於模版中的響應式變量(下圖中的變量a)內部都會保存這個渲染watcher(由於這些變量都有可能修改視圖),一旦變量被修改了就會觸發setter,最後都會再次執行updateComponent函數來刷新視圖

mounted

實例化渲染watcher渲染出頁面後會進入一個判斷,這裏要注意的是,只有根實例纔會爲true而且觸發mounted鉤子,那組件實例何時觸發mounted鉤子呢?

這裏先給出答案,在src/core/vdom/create-component.js的insert鉤子(組件專屬的vnode鉤子),同時Vue會聲明一個insertedVnodeQueue數組,保存全部的組件vnode,每當一個組件vnode被渲染成DOM節點就會往這個數組裏添加一個vnode元素,當組件所有渲染完畢後,會以子=>父的順序依次觸發mounted鉤子(最早觸發最裏層組件的mounted鉤子)。隨後再回到_init方法,最後觸發根實例的mounted鉤子,具體爲何會這麼作有興趣的同窗能夠再深刻研究

至此全部的數據都被初始化,而且渲染出了DOM節點,接下來會介紹組件更新和組件銷燬的過程

組件更新

回到mountComponent那張圖,在實例化渲染watcher的時候,Vue會給渲染watcher傳入一個對象,對象包含了一個before方法,執行before方法就會執行beforeUpdate鉤子,那何時執行這個方法呢?

一旦模版的依賴的變量發生了變化,說明即將改變視圖,會觸發setter而後執行渲染watcher的回調,即updateComponent刷新視圖,在執行這個回調前,Vue會查看是否有before這個方法,若是有則會優先執行before,而後再執行updateCompont刷新視圖

Vue會將全部的watcher放入一個隊列,flushSchedulerQueue會依次遍歷這些watcer,而渲染watcher會有一個before方法,從而觸發beforeUpdate鉤子

而後當全部的watcher都遍歷過以後,表明數據已經更新完畢,而且視圖也刷新了,此時會調用callUpdatedHooks,執行updated鉤子

組件銷燬

組件銷燬的前提是發生了視圖更新,Vue會判斷生成新視圖的vnode和舊視圖對應的vnode的區別,而後刪除那些視圖中不須要渲染的節點,這個過程最終會調用實例的$destroy方法,對應源代碼的src/core/instance/lifecycle.js

依次按照順序執行:

  1. 首先會直接執行beforeDestory的鉤子,表示準備開始銷燬節點,此時是能夠和當前組件實例交互的最後時機
  2. 隨後會找到當前組件的父節點,從父節點的children屬性中刪除當前的節點
  3. 對渲染watcher進行註銷(vm._watcher存放的是每一個組件惟一的渲染watcher)
  4. 對其餘的watcher進行註銷(user watcher,computed watcher)
  5. 清除這個實例渲染出的DOM節點
  6. 執行destroyed鉤子
  7. 註銷全部的監聽事件($off不傳參數會清空全部的監聽事件)

總結

至此整個Vue的生命週期結束了,最後再總結一下每一個生命週期主要都作了什麼事情,嚴格按照Vue內部的執行順序羅列

  • beforeCreate:將開發者定義的配置項和Vue內部的配置項進行合併,初始化組件的自定義事件,定義createElement函數/初始化插槽
  • created:初始化inject,初始化全部數據(props -> methods -> data -> computed -> watch),初始化provide
  • beforeMount:尋找是否有掛載的節點,根據render函數準備開始渲染頁面/實例化渲染watcher
  • mounted:頁面渲染完成
  • beforeUpdate:渲染watcher依賴的變量發生變化,準備更新視圖
  • updated:視圖和數據所有更新完畢
  • beforeDestroy:註銷watcher,刪除DOM節點
  • destroyed:註銷全部監聽事件

事實上要想徹底瞭解Vue的生命週期,還須要瞭解其餘方面的知識點,例如組件掛載,響應式原理,另外可能還須要瞭解一下Vue的編譯原理,每一個知識點又能夠展開十幾個小的知識點,可是當你可以真正理解Vue.js的核心原理,我相信對我的成長來講是一個不小的收穫(終於寫完了脖子都酸了_:(´°ω°`」 ∠):_)

砥礪前行 將來可期

參考資料

Vue.js 技術揭祕

相關文章
相關標籤/搜索