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

image

前言

使用Vue在日常開發(fā)中會頻繁接觸和使用生命周期,在官方文檔中是這么解釋生命周期的:

每個 Vue 實例在被創(chuàng)建時都要經(jīng)過一系列的初始化過程——例如,需要設置數(shù)據(jù)監(jiān)聽、編譯模板、將實例掛載到 DOM 并在數(shù)據(jù)變化時更新 DOM 等。同時在這個過程中也會運行一些叫做生命周期鉤子的函數(shù),這給了用戶在不同階段添加自己的代碼的機會。

好比人的生老病死的過程,Vue同樣也有從組建初始化到組件掛載,組件更新,組件銷毀的一系列過程,而生命周期鉤子,是一個函數(shù),可以讓開發(fā)者在Vue到達某個時間段的時候做一些事情

最常見的就是在mounted鉤子中發(fā)送ajax請求獲取當前的頁面組件所需要的數(shù)據(jù)

image

但是對于Vue.js進階來說,只知道生命周期的拼寫和對應的觸發(fā)時機肯定是不夠的,為什么鉤子函數(shù)不能是一個箭頭函數(shù),為什么在data中有時候無法獲取定義的數(shù)據(jù),我們通過this獲取data中的數(shù)據(jù)真的直接保存在this下了嗎,Vue又是怎么做到無感知的事件監(jiān)聽/事件解綁

在這篇文章中,我將會帶大家深入Vue的源碼,從源碼中分析Vue的生命周期

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

Vue版本:2.5.21

源碼概覽

image

當我們在main.js中實例化Vue的時候,會經(jīng)過一些邏輯,然后進入到_init函數(shù)開始Vue的生命周期,其實從這些函數(shù)的命名方式中就能大致看出Vue是如何運行的了,接下來我們逐個分析每個函數(shù)具體做了什么

合并配置項

從上面的圖中能看到,在生命周期中第一件事就是合并配置項,而對于根實例和組件實例,Vue的處理方式是不同的(在main.js中new Vue生成的是根實例,其余全部都是組件實例),根實例傳入的options參數(shù)里不會有_isComponent屬性,反之為true(實例化的時機不同,傳入的參數(shù)也不同,感興趣的朋友可以查看相關實例化的文章)

image

為了不必要的干涉,這里沒有引入vue-router,vuex

根實例合并配置項

對于根實例會走false的邏輯,進入mergeOptions函數(shù),合并Vue的各個配置項options,比如mixins,props,methods,watch,computed,生命周期鉤子等等,這是整個項目中第一次的合并配置。Vue會將所有的合并策略都保存在一個strats對象中,然后依次遍歷當前實例和parent的同一個屬性,再去starts找那個屬性對應的合并策略

通過斷點可以看到strats保存了很多合并的策略


image

我們沒有必要每個合并策略都去看一遍,盡量把精力放在整個流程中,不要撿了芝麻丟了西瓜。第一次的合并中,Vue會通過resolveConstructorOptions(vm.constructor)獲取Vue構(gòu)造器的靜態(tài)屬性options作為parent,這個options包含了一些預先設置好的配置項,而child就是我們給根實例實例化的時候傳入的一些參數(shù),對應例子中上圖的render函數(shù)

Vue預先設置的配置項作為第一次的parent:


image

根實例實例化傳入的參數(shù):

image

根實例的合并策略其實很簡單,主要就是把Vue框架內(nèi)置的一些配置項和開發(fā)者在main.js中實例化Vue構(gòu)造器傳入的參數(shù)進行一次簡單的合并,作為根實例的$options屬性

組件實例合并配置項

組件實例合并配置項并不在_init函數(shù)中,因為組件實例和根實例不同,組件實例是由組件構(gòu)造器實例化的,而根實例是由Vue構(gòu)造器實例化的,而組件構(gòu)造器又是繼承自Vue的它需要通過Vue.extend方法去繼承Vue構(gòu)造函數(shù),我畫了張圖方便理解

image

Vue這么做符合面向?qū)ο蟮脑O計模式,一個組件實質(zhì)上是一個構(gòu)造器函數(shù)(進一步可以認為是一個class),這樣在一個頁面中引入多個相同的組件只需要多次實例化組件構(gòu)造器就可以了,并且可以做到實例之間互相獨立

而面向?qū)ο罅硗庖粋€好處就是可以實現(xiàn)繼承,體現(xiàn)在Vue框架中則是將組件構(gòu)造器繼承Vue構(gòu)造器,從而組件構(gòu)造器能夠獲得Vue構(gòu)造器內(nèi)置的一些配置項

組件實例合并配置項在src/core/global-api/extend.js,同樣會調(diào)用mergeOptions組件實例合并配置項會將Vue框架內(nèi)置的配置項和當前組件配置項進行合并并賦值給組件構(gòu)造器的靜態(tài)屬性options

image

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

image
image

這里我用了父級而不是父組件,因為Vue的組件一般繼承自Vue構(gòu)造函數(shù)而不是父組件,通過流程圖可以發(fā)現(xiàn),Vue會保證生命周期函數(shù)始終是一個數(shù)組,并且以父=>子的順序排列的,Vue在執(zhí)行某個生命周期的時候會遍歷這個數(shù)組依次執(zhí)行函數(shù),所以當我們在Vue構(gòu)造器和組件構(gòu)造器中的同一個生命周期里都定義了生命周期函數(shù),會先執(zhí)行Vue構(gòu)造器中的那個

繼承了Vue構(gòu)造器后才會實例化子組件生成組件實例,再進入到_init函數(shù),這個時候_isComponent為true會執(zhí)行initInternalComponent,它會給組件實例創(chuàng)建$options屬性,指向子組件構(gòu)造器的靜態(tài)屬性options,這樣就能夠通過組件實例的$options屬性訪問到當前組件的配置項以及Vue框架內(nèi)置的配置項(包括全局組件,全局混入)

小結(jié)

  • 生命周期中第一件事就是合并配置項,對于根實例和組件實例合并的時機不同
  • 根實例是在new Vue的時候進行合并,將Vue內(nèi)置的配置項和new Vue傳入的配置項進行合并
  • 對于組件實例來說,先會創(chuàng)建子組件的構(gòu)造器,并且調(diào)用Vue.extend繼承Vue構(gòu)造器,繼承的時候?qū)ue內(nèi)置的配置項和組件配置項進行合并,并將結(jié)果保存在構(gòu)造器的options屬性中,之后在創(chuàng)建組件實例的時候進入initInternalComponent方法會將組件實例的$options指向組件構(gòu)造器的options屬性
  • Vue框架會根據(jù)不同的配置執(zhí)行不同的合并策略

代理開發(fā)環(huán)境的錯誤

非生產(chǎn)環(huán)境下會進入initProxy函數(shù),通過ES6的Proxy給vm實例做一層攔截,主要作用是給開發(fā)環(huán)境下一些不合理的配置做出一些自定義的警告

image

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

初始化自定義事件

隨后進入initLifecycle,這部分沒什么好講的,初始化實例的一些生命周期的狀態(tài)和一些額外屬性,接著會進入初始化組件的自定義事件

image

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

進入updateComponentListeners,發(fā)現(xiàn)Vue會調(diào)用add函數(shù)注冊所有的自定義事件,而對于組件來說add函數(shù)就會調(diào)用$on來達到監(jiān)聽自定義事件的效果

//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
//調(diào)用add注冊自定義事件(后面3個參數(shù)可忽略)
add(event.name, cur, event.capture, event.passive, event.params)

beforeCreate

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

接著會執(zhí)行callHook(vm, 'beforeCreate'),從字面上來看就能猜出Vue在這個時候會調(diào)用beforeCreate這個生命周期函數(shù),在之前合并配置項的時候就提到,生命周期函數(shù)最終會被包裹成一個數(shù)組,所以事實上Vue也支持這么寫

image

callHook函數(shù)會根據(jù)傳入的參數(shù)拿到$options屬性中對應的生命周期函數(shù)組成的數(shù)組,這里傳入了beforeCreate,所以會獲得beforeCreate中定義的所有生命周期函數(shù),之后順序遍歷并且用call方法給每個生命周期函數(shù)綁定了this上下文,這就是為什么生命周期函數(shù)不能使用剪頭函數(shù)書寫的原因

image

初始化數(shù)據(jù)

接著執(zhí)行initInjections,這部分是用來初始化inject這個api,由于日常開發(fā)使用頻率較少就不詳細解釋了(其實是我懶得研究-.-)

隨后會進入另外一個關鍵的函數(shù)initState,它會依次初始化props,methods,data,computed,watch,我們一個個來講解

props

組件之間通信的時候,父組件給子組件傳參,子組件需要定義props來接受父組件傳過來的屬性,而Vue規(guī)定,子組件是不能修改父組件傳來的props,因為這違背了單項數(shù)據(jù)流,會導致組件之間非常難以管理,如果在子組件修改了props,Vue會發(fā)出一個警告

而Vue又是怎么知道開發(fā)者修改了props的屬性呢?原因還是利用了訪問器描述符setter

image

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

methods

對于methods,Vue會定義一些開發(fā)過程中的不規(guī)范的警告,隨后會將所有的method綁定vm實例,這樣我們就可以直接通過this獲取當前的vm實例

data

到了最關鍵的data,data中一般保存的是當前組件需要使用的數(shù)據(jù),除了根實例之外,組件實例的data一般都是一個函數(shù),因為JS引用類型的特點,如果使用對象,當存在多個相同的組件,其中一個組件修改了data數(shù)據(jù),會反映到所有的組件。當data作為一個函數(shù)返回一個對象時,每次執(zhí)行都會生成一個新的對象,可以有效的解決這個問題

image

初始化data會執(zhí)行initData這個函數(shù),內(nèi)部會執(zhí)行定義的data函數(shù)并且把當前實例作為this值,并且賦值給_data這個內(nèi)部屬性,值得注意的是,在執(zhí)行data函數(shù)的過程中是獲取不到computed中的數(shù)據(jù),因為computed中的數(shù)據(jù)此時還沒初始化

image

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

有人會問,那為啥不直接寫在vm實例上呢?因為我們需要將數(shù)據(jù)放在一個統(tǒng)一的對象上進行管理,為的是下一步把_data通過observe變成一個響應式對象。而為了在開發(fā)的時候書寫更加簡潔,Vue采取了這種方法,非常的討巧

computed

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

image

watch

初始化watch的時候最終會調(diào)用$watch方法,生成一個user watcher,當監(jiān)聽的屬性發(fā)生改變就會立即通知user watcher執(zhí)行回調(diào)

created

再調(diào)用initProvide初始化provide后就會執(zhí)行callHook(vm, 'beforeCreate'),和beforeCreate一樣,依次遍歷定義在$options上的created數(shù)組,執(zhí)行生命周期函數(shù)

至此整個組件創(chuàng)建完畢,其實這個時候就可以和后端進行交互獲取數(shù)據(jù)了,但是對于真正的DOM節(jié)點還沒有被渲染出來,一些需要和DOM的交互操作還無法在created鉤子中執(zhí)行,即無法在created鉤子中有操作生成視圖的DOM

掛載過程

回到_init函數(shù),已經(jīng)到了最后一行,會判斷$options是否有el屬性,在Vue-cli2的時候,cli會自動在new Vue的時候傳入el參數(shù),而對于Vue-cli3并沒有這么做,而是生成根實例后主動調(diào)用$mount并傳入了掛載的節(jié)點,其實兩者都是一樣的,也可以使用$mount來實現(xiàn)組件的手動掛載

Vue-cli2:


image

Vue-cli3:


image

$mount最終會執(zhí)行mountComponent這個函數(shù)

image

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

image

組件掛載我這里不會展開詳解,盡量把重心放在生命周期方面,有興趣的朋友可以自行了解,或者看我底下的鏈接

beforeMount

當組件執(zhí)行$mount并且擁有掛載點和渲染函數(shù)的時候,就會觸發(fā)beforeMount的鉤子,準備組件的掛載

渲染視圖的函數(shù)updateComponent

之后Vue會定義一個updateComponent函數(shù),這個函數(shù)是整個掛載的核心,它由2部分組成,_render函數(shù)和_update函數(shù)

  • render函數(shù)最終會執(zhí)行之前在initRender定義的createElement函數(shù),作用是創(chuàng)建vnode
  • update函數(shù)會將上面的render函數(shù)生成的vnode渲染成一個真實的DOM樹,并掛載到掛載點上

第一次執(zhí)行updateComponent會渲染出整個DOM樹,這個時候頁面就完整的被展現(xiàn)了

渲染watcher

然后會實例化一個"渲染watcher",將updateComponent作為回調(diào)函數(shù)傳入,內(nèi)部會立即執(zhí)行一次updateComponet函數(shù)

watcher顧名思義是用來觀察的,渲染watcher簡而言之,就是會觀察模版中依賴變量的是否變化來決定是否需要刷新頁面,而updateComponet就是一個用來更新頁面的函數(shù),所以將這個函數(shù)作為回調(diào)傳入。對于模版中的響應式變量(下圖中的變量a)內(nèi)部都會保存這個渲染watcher(因為這些變量都有可能修改視圖),一旦變量被修改了就會觸發(fā)setter,最后都會再次執(zhí)行updateComponent函數(shù)來刷新視圖

image

mounted

實例化渲染watcher渲染出頁面后會進入一個判斷,這里要注意的是,只有根實例才會為true并且觸發(fā)mounted鉤子,那組件實例什么時候觸發(fā)mounted鉤子呢?

這里先給出答案,在src/core/vdom/create-component.js的insert鉤子(組件專屬的vnode鉤子),同時Vue會聲明一個insertedVnodeQueue數(shù)組,保存所有的組件vnode,每當一個組件vnode被渲染成DOM節(jié)點就會往這個數(shù)組里添加一個vnode元素,當組件全部渲染完畢后,會以子=>父的順序依次觸發(fā)mounted鉤子(最先觸發(fā)最里層組件的mounted鉤子)。隨后再回到_init方法,最后觸發(fā)根實例的mounted鉤子,具體為什么會這么做有興趣的同學可以再深入研究

image

至此所有的數(shù)據(jù)都被初始化,并且渲染出了DOM節(jié)點,接下來會介紹組件更新和組件銷毀的過程

組件更新

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

一旦模版的依賴的變量發(fā)生了變化,說明即將改變視圖,會觸發(fā)setter然后執(zhí)行渲染watcher的回調(diào),即updateComponent刷新視圖,在執(zhí)行這個回調(diào)前,Vue會查看是否有before這個方法,如果有則會優(yōu)先執(zhí)行before,然后再執(zhí)行updateCompont刷新視圖

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

image

然后當所有的watcher都遍歷過之后,代表數(shù)據(jù)已經(jīng)更新完畢,并且視圖也刷新了,此時會調(diào)用callUpdatedHooks,執(zhí)行updated鉤子

組件銷毀

組件銷毀的前提是發(fā)生了視圖更新,Vue會判斷生成新視圖的vnode和舊視圖對應的vnode的區(qū)別,然后刪除那些視圖中不需要渲染的節(jié)點,這個過程最終會調(diào)用實例的$destroy方法,對應源代碼的src/core/instance/lifecycle.js

image

依次按照順序執(zhí)行:

  1. 首先會直接執(zhí)行beforeDestory的鉤子,表示準備開始銷毀節(jié)點,此時是可以和當前組件實例交互的最后時機
  2. 隨后會找到當前組件的父節(jié)點,從父節(jié)點的children屬性中刪除當前的節(jié)點
  3. 對渲染watcher進行注銷(vm._watcher存放的是每個組件唯一的渲染watcher)
  4. 對其他的watcher進行注銷(user watcher,computed watcher)
  5. 清除這個實例渲染出的DOM節(jié)點
  6. 執(zhí)行destroyed鉤子
  7. 注銷所有的監(jiān)聽事件($off不傳參數(shù)會清空所有的監(jiān)聽事件)

總結(jié)

至此整個Vue的生命周期結(jié)束了,最后再總結(jié)一下每個生命周期主要都做了什么事情,嚴格按照Vue內(nèi)部的執(zhí)行順序羅列

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

事實上要想完全了解Vue的生命周期,還需要了解其他方面的知識點,例如組件掛載,響應式原理,另外可能還需要了解一下Vue的編譯原理,每個知識點又可以展開十幾個小的知識點,但是當你能夠真正理解Vue.js的核心原理,我相信對個人成長來說是一個不小的收獲(終于寫完了脖子都酸了:(′°ω°`」 ∠):

砥礪前行 未來可期

參考資料

Vue.js 技術(shù)揭秘

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容