1. 程式人生 > >一起學習vue原始碼 - Vue2.x的生命週期(初始化階段)

一起學習vue原始碼 - Vue2.x的生命週期(初始化階段)

> 作者:小土豆biubiubiu > > 部落格園:https://www.cnblogs.com/HouJiao/ > > 掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d > > 簡書:https://www.jianshu.com/u/cb1c3884e6d5 > > 微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術) > > 歡迎大家掃描微信二維碼進入群聊討論(若二維碼失效可新增微信JEmbrace拉你進群): > > ![](https://user-gold-cdn.xitu.io/2020/3/31/1712fabb576f52e4?w=168&h=168&f=png&s=10818) > 碼字不易,點贊鼓勵喲~ # 溫馨提示 本篇文章內容過長,一次看完會有些乏味,建議大家可以先收藏,分多次進行閱讀,這樣更好理解。 # 前言 相信很多人和我一樣,在剛開始瞭解和學習`Vue`生命明週期的時候,會做下面一系列的總結和學習。 ### 總結1 `Vue`的例項在建立時會經過一系列的初始化: 設定資料監聽、編譯模板、將例項掛載到DOM並在資料變化時更新DOM等 ### 總結2 在這個初始化的過程中會執行一些叫做"生命週期鉤子"的函式: beforeCreate:元件建立前 created:元件建立完畢 beforeMount:元件掛載前 mounted:元件掛載完畢 beforeUpdate:元件更新之前 updated:元件更新完畢 beforeDestroy:元件銷燬前 destroyed:元件銷燬完畢 ### 示例1 關於每個鉤子函式裡元件的狀態示例: ```html Vue的生命週期

{{info}}

``` ### 總結3: 結合前面示例1的執行結果會有如下的總結。 ##### 元件建立前(beforeCreate) ![](https://user-gold-cdn.xitu.io/2020/3/23/171061381bd35710?w=370&h=127&f=png&s=2525) 元件建立前,元件需要掛載的DOM元素el和元件的資料data都未被建立。 ##### 元件建立完畢(created) ![](https://user-gold-cdn.xitu.io/2020/3/23/1710625a35847c04?w=487&h=282&f=png&s=10846) 建立建立完畢後,元件的資料已經建立成功,但是DOM元素el還沒被建立。 ##### 元件掛載前(beforeMount): ![](https://user-gold-cdn.xitu.io/2020/3/23/17106271481df1bf?w=544&h=370&f=png&s=18533) 元件掛載前,DOM元素已經被建立,只是data中的資料還沒有應用到DOM元素上。 ##### 元件掛載完畢(mounted) ![](https://user-gold-cdn.xitu.io/2020/3/23/171062919bec4076?w=502&h=369&f=png&s=15751) 元件掛載完畢後,data中的資料已經成功應用到DOM元素上。 ##### 元件更新前(beforeUpdate) ![](https://user-gold-cdn.xitu.io/2020/3/23/171062ee2a428d7b?w=714&h=370&f=png&s=25396) 元件更新前,data資料已經更新,元件掛載的DOM元素的內容也已經同步更新。 ##### 元件更新完畢(updated) ![](https://user-gold-cdn.xitu.io/2020/3/23/171062f0bdcbb925?w=733&h=369&f=png&s=24996) 元件更新完畢後,data資料已經更新,元件掛載的DOM元素的內容也已經同步更新。 (感覺和beforeUpdate的狀態基本相同) ##### 元件銷燬前(beforeDestroy) ![](https://user-gold-cdn.xitu.io/2020/3/23/171063700615891d?w=764&h=372&f=png&s=29450) 元件銷燬前,元件已經不再受vue管理,我們可以繼續更新資料,但是模板已經不再更新。 ##### 元件銷燬完畢(destroyed) ![](https://user-gold-cdn.xitu.io/2020/3/23/171063913c6df20f?w=749&h=372&f=png&s=30296) 元件銷燬完畢,元件已經不再受vue管理,我們可以繼續更新資料,但是模板已經不再更新。 ### 元件生命週期圖示 最後的總結,就是來自`Vue`官網的生命週期圖示。 ![](https://user-gold-cdn.xitu.io/2020/3/23/171063deee776547?w=1200&h=3039&f=png&s=77677) 那到這裡,前期對`Vue`生命週期的學習基本就足夠了。那今天,我將帶大家從`Vue原始碼`瞭解`Vue2.x的生命週期的初始化階段`,開啟`Vue生命週期`的進階學習。 >
Vue官網的這張生命週期圖示非常關鍵和實用,後面我們的學習和總結都會基於這個圖示。 # 建立元件例項 對於一個元件,`Vue`框架要做的第一步就是建立一個`Vue`例項:即`new Vue()`。那`new Vue()`都做了什麼事情呢,我們來看一下`Vue`建構函式的原始碼實現。 ```javascript //原始碼位置備註:/vue/src/core/instance/index.js import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue ``` 從`Vue建構函式`的原始碼可以看到有兩個重要的內容:`if條件判斷邏輯`和`_init方法的呼叫`。那下面我們就這兩個點進行抽絲破繭,看一看它們的原始碼實現。 >
在這裡需要說明的是`index.js`檔案的引入會早於`new Vue`程式碼的執行,因此在`new Vue`之前會先執行`initMixin`、`stateMixin`、`eventsMixin`、`lifecycleMixin`、`renderMixin`。這些方法內部大致就是在為元件例項定義一些屬性和例項方法,並且會為屬性賦初值。 > > 我不會詳細去解讀這幾個方法內部的實現,因為本篇主要是分析學習`new Vue`的原始碼實現。那我在這裡說明這個是想讓大家大致瞭解一下和這部分相關的原始碼的執行順序,因為在`Vue`建構函式中呼叫的`_init`方法內部有很多例項屬性的訪問、賦值以及很多例項方法的呼叫,那這些例項屬性和例項方法就是在`index.js`引入的時候通過執行`initMixin`、`stateMixin`、`eventsMixin`、`lifecycleMixin`、`renderMixin`這幾個方法定義的。 # 建立元件例項 - if條件判斷邏輯 if條件判斷邏輯如下: ```javascript if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) { warn('Vue is a constructor and should be called with the `new` keyword') } ``` 我們先看一下`&&`前半段的邏輯。 `process`是`node`環境內建的一個`全域性變數`,它提供有關當前`Node.js`程序的資訊並對其進行控制。如果本機安裝了`node`環境,我們就可以直接在命令列輸入一下這個全域性變數。 ![](https://user-gold-cdn.xitu.io/2020/3/23/17106770b40423fb?w=623&h=541&f=png&s=15969) > 這個全域性變數包含的資訊非常多,這裡只截出了部分屬性。 對於[process的evn屬性](http://nodejs.cn/api/process.html#process_process_env) 它返回當前使用者環境資訊。但是這個資訊不是直接訪問就能獲取到值,而是需要通過設定才能獲取。 ![](https://user-gold-cdn.xitu.io/2020/3/23/171067fe815e21a7?w=246&h=46&f=png&s=1044) 可以看到我沒有設定這個屬性,所以訪問獲得的結果是`undefined`。 然後我們在看一下`Vue`專案中的`webpack`對`process.evn.NODE_EVN`的設定說明: ![](https://user-gold-cdn.xitu.io/2020/3/23/17106859d6fbe5a4?w=717&h=229&f=png&s=18149) > 執行`npm run dev`時會將`process.env.NODE_MODE`設定為`'development'` > 執行`npm run build`時會將`process.env.NODE_MODE`設定為`'production'` > 該配置在Vue專案根目錄下的`package.json scripts`中設定 所以設定`process.evn.NODE_EVN`的作用就是為了區分當前`Vue`專案的執行環境是`開發環境`還是`生產環境`,針對不同的環境`webpack`在打包時會啟用不同的`Plugin`。 `&&`前半段的邏輯說完了,在看下`&&`後半段的邏輯:`this instanceof Vue`。 這個邏輯我決定用一個示例來解釋一下,這樣會非常容易理解。 我們先寫一個`function`。 ```javascript function Person(name,age){ this.name = name; this.age = age; this.printThis = function(){ console.log(this); } //呼叫函式時,列印函式內部的this this.printThis(); } ``` 關於`JavaScript`的函式有兩種呼叫方式:以`普通函式`方式呼叫和以`建構函式`方式呼叫。我們分別以兩種方式呼叫一下`Person`函式,看看函式內部的`this`是什麼。 ```javascript // 以普通函式方式呼叫 Person('小土豆biubiubiu',18); // 以建構函式方式建立 var pIns = new Person('小土豆biubiubiu'); ``` 上面這段程式碼在瀏覽器的執行結果如下: ![](https://user-gold-cdn.xitu.io/2020/3/23/17106ee03aeb89a9?w=709&h=57&f=png&s=6418) 從結果我們可以總結: 以普通函式方式呼叫Person,Person內部的this物件指向的是瀏覽器全域性的window物件 以建構函式方式呼叫Person,Person內部的this物件指向的是創建出來的例項物件 > 這裡其實是JavaScript語言中this指向的知識點。 那我們可以得出這樣的結論:當以`建構函式`方式呼叫某個函式`Fn`時,函式內部`this instanceof Fn`邏輯的結果就是`true`。 囉嗦了這麼多,`if條件判斷的邏輯`已經很明瞭了: 如果當前是非生產環境且沒有使用new Vue的方式來呼叫Vue方法,就會有一個警告: Vue is a constructor and should be called with the `new`keyword 即Vue是一個建構函式應該使用關鍵字new來呼叫Vue # 建立元件例項 - _init方法的呼叫 `_init`方法是定義在Vue原型上的一個方法: ```javascript //原始碼位置備註:/vue/src/core/instance/init.js export function initMixin (Vue: Class) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // 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') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } if (vm.$options.el) { vm.$mount(vm.$options.el) } } } ``` > `Vue`的建構函式所在的原始檔路徑為`/vue/src/core/instance/index.js`,在該檔案中有一行程式碼`initMixin(Vue)`,該方法呼叫後就會將`_init`方法新增到Vue的原型物件上。這個我在前面提說過`index.js`和`new Vue`的執行順序,相信大家已經能理解。 那這個`_init`方法中都幹了寫什麼呢? ### vm.$options 大致瀏覽一下`_init`內部的程式碼實現,可以看到第一個就是為元件例項設定了一個`$options`屬性。 ```javascript //原始碼位置備註:/vue/src/core/instance/init.js // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } ``` 首先`if`分支的`options`變數是`new Vue`時傳遞的選項。 ![](https://user-gold-cdn.xitu.io/2020/3/31/1712e961b159cdec?w=288&h=120&f=png&s=2963) 那滿足`if`分支的邏輯就是如果`options`存在且是一個元件。那在`new Vue`的時候顯然不滿足`if`分支的邏輯,所以會執行`else`分支的邏輯。 > 使用`Vue.extend`方法建立元件的時候會滿足`if`分支的邏輯。 在else分支中,`resolveConstructorOptions`的作用就是通過元件例項的建構函式獲取當前元件的選項和父元件的選項,在通過`mergeOptions`方法將這兩個選項進行合併。 > 這裡的父元件不是指元件之間引用產生的父子關係,還是跟`Vue.extend`相關的父子關係。目前我也不太瞭解`Vue.extend`的相關內容,所以就不多說了。 ### vm._renderProxy 接著就是為元件例項的`_renderProxy`賦值。 ```javascript //原始碼位置備註:/vue/src/core/instance/init.js /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } ``` 如果是非生產環境,呼叫`initProxy`方法,生成`vm`的代理物件`_renderProxy`;否則`_renderProxy`的值就是當前元件的例項。 然後我們看一下非生產環境中呼叫的`initProxy`方法是如何為`vm._renderProxy`賦值的。 ```javascript //原始碼位置備註:/vue/src/core/instance/proxy.js const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy) initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use const options = vm.$options const handlers = options.render && options.render._withStripped ? getHandler : hasHandler vm._renderProxy = new Proxy(vm, handlers) } else { vm._renderProxy = vm } } ``` 在`initProxy`方法內部實際上是利用`ES6`中`Proxy`物件為將元件例項vm進行包裝,然後賦值給`vm._renderProxy`。 關於`Proxy`的用法如下: ![](https://user-gold-cdn.xitu.io/2020/3/31/1712f633a4679a6f?w=700&h=406&f=png&s=20257) 那我們簡單的寫一個關於`Proxy`的用法示例。 ```javascript let obj = { 'name': '小土豆biubiubiu', 'age': 18 }; let handler = { get: function(target, property){ if(target[property]){ return target[property]; }else{ console.log(property + "屬性不存在,無法訪問"); return null; } }, set: function(target, property, value){ if(target[property]){ target[property] = value; }else{ console.log(property + "屬性不存在,無法賦值"); } } } obj._renderProxy = null; obj._renderProxy = new Proxy(obj, handler); ``` 這個寫法呢,仿照原始碼給`vm`設定`Proxy`的寫法,我們給`obj`這個物件設定了`Proxy`。 根據`handler`函式的實現,當我們訪問代理物件`_renderProxy`的某個屬性時,如果屬性存在,則直接返回對應的值;如果屬性不存在則列印`'屬性不存在,無法訪問'`,並且返回`null`。 當我們修改代理物件`_renderProxy`的某個屬性時,如果屬性存在,則為其賦新值;如果不存在則列印`'屬性不存在,無法賦值'`。 接著我們把上面這段程式碼放入瀏覽器的控制檯執行,然後訪問代理物件的屬性: ![](https://user-gold-cdn.xitu.io/2020/3/31/1712f815c89bebb7?w=420&h=160&f=png&s=9002) 然後在修改代理物件的屬性: ![](https://user-gold-cdn.xitu.io/2020/3/31/1712f826166a4f2a?w=454&h=119&f=png&s=5691) 結果和我們前面描述一致。然後我們在說回`initProxy`,它實際上也就是在訪問`vm`上的某個屬性時做一些驗證,比如該屬性是否在vm上,訪問的屬性名稱是否合法等。 總結這塊的作用,實際上就是在非生產環境中為我們的程式碼編寫的程式碼做出一些錯誤提示。 ### 連續多個函式呼叫 最後就是看到有連續多個函式被呼叫。 ```javascript 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') ``` 我們把最後這幾個函式的呼叫順序和`Vue`官網的`生命週期圖示`對比一下: ![](https://user-gold-cdn.xitu.io/2020/3/24/1710a8365dd21156?w=589&h=338&f=png&s=36734) 可以發現程式碼和這個圖示基本上是一一對應的,所以`_init`方法被稱為是`Vue例項的初始化方法`。下面我們將逐個解讀`_init`內部按順序呼叫的那些方法。 # initLifecycle-初始化生命週期 ```javascript //原始碼位置備註:/vue/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 } ``` 在初始化生命週期這個函式中,`vm`是當前`Vue`元件的例項物件。我們看到函式內部大多數都是給`vm`這個例項物件的屬性賦值。 > 以`$`開頭的屬性稱為元件的`例項屬性`,在`Vue`官網中都會有明確的解釋。 `$parent`屬性表示的是當前元件的父元件,可以看到在`while`迴圈中會一直遞迴尋找第一個非抽象的父級元件:`parent.$options.abstract && parent.$parent`。 > 非抽象型別的父級元件這裡不是很理解,有夥伴知道的可以在評論區指導一下。 `$root`屬性表示的是當前元件的`跟元件`。如果當前元件存在`父元件`,那當前元件的`根元件`會繼承父元件的`$root`屬性,因此直接訪問`parent.$root`就能獲取到當前元件的根元件;如果當前元件例項不存在父元件,那當前元件的跟元件就是它自己。 `$children`屬性表示的是當前元件例項的`直接子元件`。在前面`$parent`屬性賦值的時候有這樣的操作:`parent.$children.push(vm)`,即將當前元件的例項物件新增到到父元件的`$children`屬性中。所以`$children`資料的新增規則為:當前元件為父元件的`$children`屬性賦值,那當前元件的`$children`則由其子元件來負責新增。 `$refs`屬性表示的是模板中註冊了`ref`屬性的`DOM`元素或者元件例項。 # initEvents-初始化事件 ```javascript //原始碼位置備註:/vue/src/core/instance/events.js export function initEvents (vm: Component) { // Object.create(null):建立一個原型為null的空物件 vm._events = Object.create(null) vm._hasHookEvent = false // init parent attached events const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) } } ``` ### vm._events 在初始化事件函式中,首先給`vm`定義了一個`_events`屬性,並給其賦值一個空物件。那`_events`表示的是什麼呢?我們寫一段程式碼驗證一下。 ```html Vue的生命週期

這裡是父元件App

``` 我們將這段程式碼的邏輯簡單梳理一下。 首先是`child`元件。 建立一個名為child元件的元件,在該元件中使用v-on聲明瞭兩個事件。 一個事件為triggerSelf,內部邏輯列印字串'triggerSelf'。 另一個事件為triggetParent,內部邏輯是使用$emit觸發父元件updateinfo事件。 我們還在元件的mounted鉤子函式中列印了元件例項this的值。 接著是`App`元件的邏輯。 App元件中定義了一個名為destoryComponent的事件。 同時App元件還引用了child元件,並且在子元件上綁定了一個為updateinfo的native DOM事件。 App元件的mounted鉤子函式也列印了元件例項this的值。 > 因為在`App`元件中引用了`child`元件,因此`App`元件和`child`元件構成了父子關係,且`App`元件為父元件,`child`元件為子元件。 邏輯梳理完成後,我們執行這份程式碼,檢視一下兩個元件例項中`_events`屬性的列印結果。 ![](https://user-gold-cdn.xitu.io/2020/3/26/17114c92fd0c64e5?w=409&h=43&f=png&s=3016) ![](https://user-gold-cdn.xitu.io/2020/3/26/17114cac1271b4cf?w=400&h=50&f=png&s=2974) 從列印的結果可以看到,當前元件例項的`_events`屬性儲存的只是父元件繫結在當前元件上的事件,而不是元件中所有的事件。 ### vm._hasHookEvent `_hasHookEvent`屬性表示的是父元件是否通過`v-hook:鉤子函式名稱`把鉤子函式繫結到當前元件上。 ### updateComponentListeners(vm, listeners) 對於這個函式,我們首先需要關注的是`listeners`這個引數。我們看一下它是怎麼來的。 ```javascript // init parent attached events const listeners = vm.$options._parentListeners ``` 從註釋翻譯過來的意思就是`初始化父元件新增的事件`。到這裡不知道大家是否有和我相同的疑惑,我們前面說`_events`屬性儲存的是父元件繫結在當前元件上的事件。這裡又說`_parentListeners`也是父元件新增的事件。這兩個屬性到底有什麼區別呢? 我們將上面的示例稍作修改,新增一條列印資訊`(這裡只將修改的部分貼出來)`。 ```html ``` 接著我們在瀏覽器中執行程式碼,檢視結果。 ![](https://user-gold-cdn.xitu.io/2020/3/27/1711ac7c0733adf6?w=681&h=250&f=png&s=14739) 從這個結果我們其實可以看到,`_events`和`_parentListeners`儲存的內容實際上都是父元件繫結在當前元件上的事件。只是儲存的鍵值稍微有一些區別: 區別一: 前者事件名稱這個key直接是事件名稱 後者事件名稱這個key儲存的是一個字串和事件名稱的拼接,這個字串是對修飾符的一個轉化(.once修飾符會轉化為~;.capture修飾符會轉化為!) 區別二: 前者事件名稱對應的value是一個數組,數組裡面才是對應的事件回撥 後者事件名稱對應的vaule直接就是回撥函式 Ok,繼續我們的分析。 接著就是判斷這個`listeners`:假如`listeners`存在的話,就執行`updateComponentListeners(vm, listeners)`方法。我們看一下這個方法內部實現。 ```javascript //原始碼位置備註:/vue/src/core/instance/events.js export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) { target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm) target = undefined } ``` 可以看到在該方法內部又呼叫到了`updateListeners`,先看一下這個函式的引數吧。 `listeners`:這個引數我們剛說過,是父元件中新增的事件。 `oldListeners`:這引數根據變數名翻譯就是舊的事件,具體是什麼目前還不太清楚。但是在初始化事件的整個過程中,呼叫到`updateComponentListeners`時傳遞的`oldListeners`引數值是一個空值。所以這個值我們暫時不用關注。(在`/vue/src/`目錄下全域性搜尋`updateComponentListeners`這個函式,會發現該函式在其他地方有呼叫,所以該引數應該是在別的地方有用到)。 `add`: add是一個函式,函式內部邏輯程式碼為: ```javascript function add (event, fn) { target.$on(event, fn) } ``` `remove`: remove也是一個函式,函式內部邏輯程式碼為: ```javascript function remove (event, fn) { target.$off(event, fn) } ``` `createOnceHandler`: `vm`:這個引數就不用多說了,就是當前元件的例項。 這裡我們主要說一下add函式和remove函式中的兩個重要程式碼:`target.$on`和`target.$off`。 首先`target`是在`event.js`檔案中定義的一個全域性變數: ```javascript //原始碼位置備註:/vue/src/core/instance/events.js let target: any ``` 在`updateComponentListeners`函式內部,我們能看到將元件例項賦值給了`target`: ```javascript //原始碼位置備註:/vue/src/core/instance/events.js target = vm ``` 所以`target`就是元件例項。當然熟悉`Vue`的同學應該很快能反應上來`$on`、`$off`方法本身就是定義在元件例項上和事件相關的方法。那元件例項上有關事件的方法除了`$on`和`$off`方法之外,還有兩個方法:`$once`和`$emit`。 在這裡呢,我們暫時不詳細去解讀這四個事件方法的原始碼實現,只截圖貼出`Vue`官網對這個四個例項方法的用法描述。 ##### vm.$on ![](https://user-gold-cdn.xitu.io/2020/3/26/17116041de20ab03?w=734&h=493&f=png&s=26499) ##### vm.$once ![](https://user-gold-cdn.xitu.io/2020/3/26/1711604d34990221?w=625&h=254&f=png&s=11077) ##### vm.$emit ![](https://user-gold-cdn.xitu.io/2020/3/26/17116057fdce415d?w=501&h=212&f=png&s=9858) > vm.$emit的用法在 [Vue父子元件通訊](https://juejin.im/post/5e61c014e51d45270f52c9e6) 一文中有詳細的示例。 ##### vm.$off ![](https://user-gold-cdn.xitu.io/2020/3/26/17116055d27e51e4?w=528&h=387&f=png&s=20597) `updateListeners`函式的引數基本解釋完了,接著我們在迴歸到`updateListeners`函式的內部實現。 ```javascript //原始碼位置備註:/vue/src/vdom/helpers/update-listener.js export function updateListeners ( on: Object, oldOn: Object, add: Function, remove: Function, createOnceHandler: Function, vm: Component ) { let name, def, cur, old, event // 迴圈斷當前元件的父元件上的事件 for (name in on) { // 根據事件名稱獲取事件回撥函式 def = cur = on[name] // oldOn引數對應的是oldListeners,前面說過這個引數在初始化的過程中是一個空物件{},所以old的值為undefined old = oldOn[name] event = normalizeEvent(name) if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur, vm) } if (isTrue(event.once)) { cur = on[name] = createOnceHandler(event.name, cur, event.capture) } // 將父級的事件新增到當前元件的例項中 add(event.name, cur, event.capture, event.passive, event.params) } } } ``` 首先是`normalizeEvent`這個函式,該函式就是對事件名稱進行一個分解。假如事件名稱`name='updateinfo.once'`,那經過該函式分解後返回的`event`物件為: ```javascript { name: 'updateinfo', once: true, capture: false, passive: false } ``` > 關於`normalizeEvent`函式內部的實現也非常簡單,這裡就直接將結論整理出來。感興趣的同學可以去看下原始碼實現,原始碼所在位置:`/vue/src/vdom/helpers/update-listener.js`。 接下來就是在迴圈父元件事件的時候做一些`if/else`的條件判斷,將父元件繫結在當前元件上的事件新增到當前元件例項的`_events`屬性中;或者從當前元件例項的`_events`屬性中移除對應的事件。 > `將父元件繫結在當前元件上的事件新增到當前元件的_events屬性中`這個邏輯就是`add`方法內部呼叫`vm.$on`實現的。詳細可以去看下`vm.$on`的原始碼實現,這裡不再多說。而且從`vm.$on`函式的實現,也能看出`_events`和`_parentListener`之間的關聯和差異。 # initRender-初始化模板 ```javascript //原始碼位置備註:/vue/src/core/instance/render.js export function initRender (vm: Component) { vm._vnode = null // the root of the child tree vm._staticTrees = null // v-once cached trees const options = vm.$options const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree const renderContext = parentVnode && parentVnode.context vm.$slots = resolveSlots(options._renderChildren, renderContext) vm.$scopedSlots = emptyObject //將createElement fn繫結到元件例項上 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // normalization is always applied for the public version, used in // user-written render functions. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) // $attrs & $listeners are exposed for easier HOC creation. // they need to be reactive so that HOCs using them are always updated const parentData = parentVnode && parentVnode.data /* 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) } } ``` `initRender`函式中,基本上是在為元件例項vm上的屬性賦值:`$slots`、`$scopeSlots`、`$createElement`、`$attrs`、`$listeners`。 那接下來就一一分析一下這些屬性就知道`initRender`在執行的過程的邏輯了。 ### vm.$slots ![](https://user-gold-cdn.xitu.io/2020/3/27/1711b28dd23c2471?w=726&h=379&f=png&s=28020) 這是來自官網對`vm.$slots`的解釋,那為了方便,我還是寫一個示例。 ```html Vue的生命週期

App元件,slot='root'

這裡是slot=first

這裡是slot=first

這裡沒有設定slot

這裡是slot=last

``` 執行程式碼,看一下結果。 ![](https://user-gold-cdn.xitu.io/2020/3/27/1711b605912ca6ca?w=572&h=409&f=png&s=20667) 可以看到,`child`元件的`vm.$slots`列印結果是一個包含三個鍵值對的物件。其中`key`為`first`的值儲存了兩個`VNode`物件,這兩個`Vnode`物件就是我們在引用`child`元件時寫的`slot=first`的兩個`h3`元素。那`key`為`last`的值也是同樣的道理。 > `key`為`default`的值儲存了四個`Vnode`,其中有一個是引用`child`元件時寫沒有設定`slot`的那個`h3`元素,另外三個`Vnode`實際上是四個`h3`元素之間的換行,假如把`child`內部的`h3`這樣寫: ```html

這裡是slot=first

這裡是slot=first

這裡沒有設定slot

這裡是slot=last

``` > 那最終列印`key`為`default`對應的值就只包含我們沒有設定`slot`的`h1`元素。 所以原始碼中的`resolveSlots`函式就是解析模板中父元件傳遞給當前元件的`slot`元素,並且轉化為`Vnode`賦值給當前元件例項的`$slots`物件。 ### vm.$scopeSlots `vm.$scopeSlots`是`Vue`中作用域插槽的內容,和`vm.$slot`查不多的原理,就不多說了。 > 在這裡暫時給`vm.$scopeSlots`賦值了一個空物件,後續會在掛載元件呼叫`vm.$mount`時為其賦值。 ### vm.$createElement `vm.$createElement`是一個函式,該函式可以接收兩個引數: 第一個引數:HTML元素標籤名 第二個引數:一個包含Vnode物件的陣列 `vm.$createElement`會將`Vnode`物件陣列中的`Vnode`元素編譯成為`html`節點,並且放入第一個引數指定的`HTML`元素中。 那前面我們講過`vm.$slots`會將父元件傳遞給當前元件的`slot`節點儲存起來,且對應的`slot`儲存的是包含多個`Vnode`物件的陣列,因此我們就藉助`vm.$slots`來寫一個示例演示一下`vm.$createElement`的用法。 ```html Vue的生命週期

App元件,slot='root'

這裡是slot=first

這裡是slot=first

這裡沒有設定slot

這裡是slot=last

``` 這個示例程式碼和前面介紹`vm.$slots`的程式碼差不多,就是在建立子元件時編寫了`render`函式,並且使用了`vm.$createElement`返回模板的內容。那我們瀏覽器中的結果。 ![](https://user-gold-cdn.xitu.io/2020/3/27/1711ba6d350762fb?w=502&h=496&f=png&s=23585) 可以看到,正如我們所說,`vm.$createElement`將`$slots`中`frist`對應的 `包含兩個Vnode物件的陣列`編譯成為兩個`h3`元素,並且放入第一個引數指定的`p`元素中,在經過子元件的`render`函式將`vm.$createElement`的返回值進行處理,就看到了瀏覽器中展示的效果。 > `vm.$createElement` 內部實現暫時不深入探究,因為牽扯到`Vue`中`Vnode`的內容,後面瞭解`Vnode`後在學習其內部實現。 ### vm.$attr和vm.$listener 這兩個屬性是有關元件通訊的例項屬性,賦值方式也非常簡單,不在多說。 # callHook(beforeCreate)-呼叫生命週期鉤子函式 `callhook`函式執行的目的就是呼叫`Vue`的生命週期鉤子函式,函式的第二個引數是一個`字串`,具體指定呼叫哪個鉤子函式。那在初始化階段,順序執行完 `initLifecycle`、`initState`、`initRender`後就會呼叫`beforeCreate`鉤子函式。 接下來看下原始碼實現。 ```javascript //原始碼位置備註:/vue/src/core/instance/lifecycle.js export function callHook (vm: Component, hook: string) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget() // 根據鉤子函式的名稱從元件例項中獲取元件的鉤子函式 const handlers = vm.$options[hook] const info = `${hook} hook` if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { invokeWithErrorHandling(handlers[i], vm, null, vm, info) } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook) } popTarget() } ``` 首先根據鉤子函式的名稱從元件例項中獲取元件的鉤子函式,接著呼叫`invokeWithErrorHandling`,`invokeWithErrorHandling`函式的第三個引數為null,所以`invokeWithErrorHandling`內部就是通過apply方法實現鉤子函式的呼叫。 > 我們應該看到原始碼中是迴圈`handlers`然後呼叫`invokeWithErrorHandling`函式。那實際上,我們在編寫元件的時候是可以`寫多個名稱相同的鉤子`,但是實際上`Vue`在處理的時候只會在例項上保留最後一個重名的鉤子函式,那這個迴圈的意義何在呢? > > 為了求證,我在`beforeCrated`這個鉤子中列印了`this.$options['before']`,然後發現這個結果是一個數組,而且只有一個元素。 > ![](https://user-gold-cdn.xitu.io/2020/3/30/17129634749300c3?w=335&h=90&f=png&s=3434) > 這樣想來就能理解這個迴圈的寫法了。 # initInjections-初始化注入 initInjections這個函式是個Vue中的inject相關的內容。所以我們先看一下[官方文件度對inject的解釋](https://cn.vuejs.org/v2/api/#provide-inject)。 ![](https://user-gold-cdn.xitu.io/2020/3/30/171297502c34d0cc?w=761&h=491&f=png&s=28800) 官方文件中說`inject`和`provide`通常是一起使用的,它的作用實際上也是父子元件之間的通訊,但是會建議大家在開發高階元件時使用。 > `provide` 是下文中`initProvide`的內容。 關於`inject`和`provide`的用法會有一個特點:只要父元件使用`provide`註冊了一個數據,那不管有多深的子元件巢狀,子元件中都能通過`inject`獲取到父元件上註冊的資料。 ![](https://user-gold-cdn.xitu.io/2020/3/30/17129858beba264b?w=603&h=528&f=png&s=24127) 大致瞭解`inject`和`provide`的用法後,就能猜想到`initInjections`函式內部是如何處理`inject`的了:解析獲取當前元件中`inject`的值,需要查詢父元件中的`provide`中是否註冊了某個值,如果有就返回,如果沒有則需要繼續向上查詢父元件。 下面看一下`initInjections`函式的原始碼實現。 ```javascript // 原始碼位置備註:/vue/src/core/instance/inject.js export function initInjections (vm: Component) { const result = resolveInject(vm.$options.inject, vm) if (result) { toggleObserving(false) Object.keys(result).forEach(key => { /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { defineReactive(vm, key, result[key], () => { warn( `Avoid mutating an injected value directly since the changes will be ` + `overwritten whenever the provided component re-renders. ` + `injection being mutated: "${key}"`, vm ) }) } else { defineReactive(vm, key, result[key]) } }) toggleObserving(true) } } ``` 原始碼中第一行就呼叫了`resolveInject`這個函式,並且傳遞了當前元件的inject配置和元件例項。那這個函式就是我們說的遞歸向上查詢父元件的`provide`,其核心程式碼如下: ```javascript // source為當前元件例項 let source = vm while (source) { if (source._provided && hasOwn(source._provided, provideKey)) { result[key] = source._provided[provideKey] break } // 繼續向上查詢父元件 source = source.$parent } ``` 需要說明的是當前元件的`_provided`儲存的是父元件使用`provide`註冊的資料,所以在`while`迴圈裡會先判斷 `source._provided`是否存在,如果該值為 `true`,則表示父元件中包含使用`provide`註冊的資料,那麼就需要進一步判斷父元件`provide`註冊的資料是否存在當前元件中`inject`中的屬性。 遞迴查詢的過程中,對弈查詢成功的資料,`resolveInject`函式會將inject中的元素對應的值放入一個字典中作為返回值返回。 例如當前元件中的`inject`設定為:`inject: ['name','age','height']`,那經過`resolveInject`函式處理後會得到這樣的返回結果: ```javascript { 'name': '小土豆biubiubiu', 'age': 18, 'height': '180' } ``` 最後在回到`initInjections`函式,後面的程式碼就是在非生產環境下,將inject中的資料變成響應式的,利用的也是雙向資料繫結的那一套原理。 # initState-初始化狀態 ```javascript //原始碼位置備註:/vue/src/core/instance/state.js export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } ``` 初始化狀態這個函式中主要會初始化`Vue`元件定義的一些屬性:`props`、`methods`、`data`、`computed`、`Watch`。 ![](https://user-gold-cdn.xitu.io/2020/3/24/1710b8edaf1bf3c4?w=763&h=730&f=png&s=54803) 我們主要看一下`data`資料的初始化,即`initData`函式的實現。 ```javascript //原始碼位置備註:/vue/src/core/instance/state.js function initData (vm: Component) { let data = vm.$options.data // 省略部分程式碼······ // observe data observe(data, true /* asRootData */) } ``` 在`initData`函式裡面,我們看到了一行熟悉系的程式碼:`observe(data)`。這個`data`引數就是`Vue`元件中定義的`data`資料。正如註釋所說,這行程式碼的作用就是`將物件變得可觀測`。 在往`observe`函式內部追蹤的話,就能追到之前 [[1W字長文+多圖,帶你瞭解vue2.x的雙向資料繫結原始碼實現]](https://juejin.im/post/5e71e7066fb9a07cab3ab804) 裡面的`Observer`的實現和呼叫。 所以現在我們就知道將物件變得可觀測就是在`Vue`例項初始化階段的`initData`這一步中完成的。 # initProvide-初始化 ```javascript //原始碼位置備註:/vue/src/core/instance/inject.js export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } } ``` 這個函式就是我們在總結`initInjections`函式時提到的`provide`。那該函式也非常簡單,就是為當前元件例項設定`_provide`。 # callHook(created)-呼叫生命週期鉤子函式 到這個階段已經順序執行完`initLifecycle`、`initState`、`initRender`、`callhook('beforeCreate')`、`initInjections`、`initProvide`這些方法,然後就會呼叫`created`鉤子函式。 > `callHook`內部實現在前面已經說過,這裡也是一樣的,所以不再重複說明。 # 總結 到這裡,Vue2.x的生命週期的`初始化階段`就解讀完畢了。這裡我們將初始化階段做一個簡單的總結。 ![](https://user-gold-cdn.xitu.io/2020/3/31/1712f98b87f33baa?w=821&h=708&f=png&s=85376) 原始碼還是很強大的,學習的過程還是比較艱難枯燥的,但是會發現很多有意思的寫法,還有我們經常看過的一些理論內容在原始碼中的真實實踐,所以一定要堅持下去。期待下一篇文章`[你還不知道Vue的生命週期嗎?帶你從Vue原始碼瞭解Vue2.x的生命週期(模板編譯階段)]`。 > 作者:小土豆biubiubiu > > 部落格園:https://www.cnblogs.com/HouJiao/ > > 掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d > > 簡書:https://www.jianshu.com/u/cb1c3884e6d5 > > 微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術) > > 歡迎大家掃描微信二維碼進入群聊討論(若二維碼失效可新增微信JEmbrace拉你進群): > > ![](https://user-gold-cdn.xitu.io/2020/3/31/1712fabb576f52e4?w=168&h=168&f=png&s=10818) > 碼字不易,點贊鼓