一起學習vue源碼 – Vue2.x的生命周期(初始化階段)

  • 2020 年 3 月 31 日
  • 筆記

作者:小土豆biubiubiu

部落格園:https://www.cnblogs.com/HouJiao/

掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d

簡書:https://www.jianshu.com/u/cb1c3884e6d5

微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術)

歡迎大家掃描微信二維碼進入群聊討論(若二維碼失效可添加微信JEmbrace拉你進群):


碼字不易,點贊鼓勵喲~

溫馨提示

本篇文章內容過長,一次看完會有些乏味,建議大家可以先收藏,分多次進行閱讀,這樣更好理解。

前言

相信很多人和我一樣,在剛開始了解和學習Vue生命明周期的時候,會做下面一系列的總結和學習。

總結1

Vue的實例在創建時會經過一系列的初始化:

設置數據監聽、編譯模板、將實例掛載到DOM並在數據變化時更新DOM等  

總結2

在這個初始化的過程中會運行一些叫做"生命周期鉤子"的函數:

beforeCreate:組件創建前  created:組件創建完畢  beforeMount:組件掛載前  mounted:組件掛載完畢  beforeUpdate:組件更新之前  updated:組件更新完畢  beforeDestroy:組件銷毀前  destroyed:組件銷毀完畢  

示例1

關於每個鉤子函數里組件的狀態示例:

<!DOCTYPE html>  <html lang="en">  <head>      <meta charset="UTF-8">      <meta name="viewport" content="width=device-width, initial-scale=1.0">      <title>Vue的生命周期</title>      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  </head>  <body>      <div id="app">          <h3>{{info}}</h3>          <button v-on:click='updateInfo'>修改數據</button>          <button v-on:click='destoryComponent'>銷毀組件</button>      </div>      <script>          var vm = new Vue({              el: '#app',              data: {                  info: 'Vue的生命周期'              },              beforeCreate: function(){                  console.log("beforeCreated-組件創建前");                  console.log("el:");                  console.log(this.$el);                  console.log("data:");                  console.log(this.$data);              },              created: function(){                  console.log("created-組件創建完畢");                  console.log("el:");                  console.log(this.$el);                  console.log("data:");                  console.log(this.$data);                  console.log("info:");                  console.log(this.$data.info);              },              beforeMount: function(){                  console.log("beforeMounted-組件掛載前");                  console.log("el:");                  console.log(this.$el);                  console.log("data:");                  console.log(this.$data);                  console.log("info:");                  console.log(this.$data.info);              },              mounted: function(){                  console.log("mounted-組件掛載完畢");                  console.log("el:");                  console.log(this.$el);                  console.log("data:");                  console.log(this.$data);                  console.log("info:");                  console.log(this.$data.info);              },              beforeUpdate: function(){                  console.log("beforeUpdate-組件更新前");                  console.log("el:");                  console.log(this.$el);                  console.log("data:");                  console.log(this.$data);                  console.log("info:");                  console.log(this.$data.info);              },              updated: function(){                  console.log("updated-組件更新完畢");                  console.log("el:");                  console.log(this.$el);                  console.log("data:");                  console.log(this.$data);                  console.log("info:");                  console.log(this.$data.info);              },              beforeDestroy: function(){                  console.log("beforeDestory-組件銷毀前");                    //在組件銷毀前嘗試修改data中的數據                  this.info="組件銷毀前";                    console.log("el:");                  console.log(this.$el);                  console.log("data:");                  console.log(this.$data);                  console.log("info:");                  console.log(this.$data.info);              },              destroyed: function(){                  console.log("destoryed-組件銷毀完畢");                    //在組件銷毀完畢後嘗試修改data中的數據                  this.info="組件已銷毀";                    console.log("el:");                  console.log(this.$el);                  console.log("data:");                  console.log(this.$data);                  console.log("info:");                  console.log(this.$data.info);              },              methods: {                  updateInfo: function(){                      // 修改data數據                      this.info = '我發生變化了'                  },                  destoryComponent: function(){                      //手動調用銷毀組件                      this.$destroy();                    }              }          });      </script>  </body>  </html>  

總結3:

結合前面示例1的運行結果會有如下的總結。

組件創建前(beforeCreate)

組件創建前,組件需要掛載的DOM元素el和組件的數據data都未被創建。  
組件創建完畢(created)

創建創建完畢後,組件的數據已經創建成功,但是DOM元素el還沒被創建。  
組件掛載前(beforeMount):

組件掛載前,DOM元素已經被創建,只是data中的數據還沒有應用到DOM元素上。  
組件掛載完畢(mounted)

組件掛載完畢後,data中的數據已經成功應用到DOM元素上。  
組件更新前(beforeUpdate)

組件更新前,data數據已經更新,組件掛載的DOM元素的內容也已經同步更新。  
組件更新完畢(updated)

組件更新完畢後,data數據已經更新,組件掛載的DOM元素的內容也已經同步更新。  (感覺和beforeUpdate的狀態基本相同)  
組件銷毀前(beforeDestroy)

組件銷毀前,組件已經不再受vue管理,我們可以繼續更新數據,但是模板已經不再更新。  
組件銷毀完畢(destroyed)

組件銷毀完畢,組件已經不再受vue管理,我們可以繼續更新數據,但是模板已經不再更新。  

組件生命周期圖示

最後的總結,就是來自Vue官網的生命周期圖示。

那到這裡,前期對Vue生命周期的學習基本就足夠了。那今天,我將帶大家從Vue源碼了解Vue2.x的生命周期的初始化階段,開啟Vue生命周期的進階學習。

Vue官網的這張生命周期圖示非常關鍵和實用,後面我們的學習和總結都會基於這個圖示。

創建組件實例

對於一個組件,Vue框架要做的第一步就是創建一個Vue實例:即new Vue()。那new Vue()都做了什麼事情呢,我們來看一下Vue構造函數的源碼實現。

//源碼位置備註:/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之前會先執行initMixinstateMixineventsMixinlifecycleMixinrenderMixin。這些方法內部大致就是在為組件實例定義一些屬性和實例方法,並且會為屬性賦初值。

我不會詳細去解讀這幾個方法內部的實現,因為本篇主要是分析學習new Vue的源碼實現。那我在這裡說明這個是想讓大家大致了解一下和這部分相關的源碼的執行順序,因為在Vue構造函數中調用的_init方法內部有很多實例屬性的訪問、賦值以及很多實例方法的調用,那這些實例屬性和實例方法就是在index.js引入的時候通過執行initMixinstateMixineventsMixinlifecycleMixinrenderMixin這幾個方法定義的。

創建組件實例 – if條件判斷邏輯

if條件判斷邏輯如下:

if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {      warn('Vue is a constructor and should be called with the `new` keyword')  }  

我們先看一下&&前半段的邏輯。

processnode環境內置的一個全局變數,它提供有關當前Node.js進程的資訊並對其進行控制。如果本機安裝了node環境,我們就可以直接在命令行輸入一下這個全局變數。

這個全局變數包含的資訊非常多,這裡只截出了部分屬性。

對於process的evn屬性 它返回當前用戶環境資訊。但是這個資訊不是直接訪問就能獲取到值,而是需要通過設置才能獲取。

可以看到我沒有設置這個屬性,所以訪問獲得的結果是undefined

然後我們在看一下Vue項目中的webpackprocess.evn.NODE_EVN的設置說明:

執行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

function Person(name,age){      this.name = name;      this.age = age;      this.printThis = function(){          console.log(this);      }      //調用函數時,列印函數內部的this      this.printThis();  }  

關於JavaScript的函數有兩種調用方式:以普通函數方式調用和以構造函數方式調用。我們分別以兩種方式調用一下Person函數,看看函數內部的this是什麼。

// 以普通函數方式調用  Person('小土豆biubiubiu',18);  // 以構造函數方式創建  var pIns = new Person('小土豆biubiubiu');  

上面這段程式碼在瀏覽器的執行結果如下:

從結果我們可以總結:

以普通函數方式調用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原型上的一個方法:

//源碼位置備註:/vue/src/core/instance/init.js  export function initMixin (Vue: Class<Component>) {    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.jsnew Vue的執行順序,相信大家已經能理解。

那這個_init方法中都幹了寫什麼呢?

vm.$options

大致瀏覽一下_init內部的程式碼實現,可以看到第一個就是為組件實例設置了一個$options屬性。

//源碼位置備註:/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時傳遞的選項。

那滿足if分支的邏輯就是如果options存在且是一個組件。那在new Vue的時候顯然不滿足if分支的邏輯,所以會執行else分支的邏輯。

使用Vue.extend方法創建組件的時候會滿足if分支的邏輯。

在else分支中,resolveConstructorOptions的作用就是通過組件實例的構造函數獲取當前組件的選項和父組件的選項,在通過mergeOptions方法將這兩個選項進行合併。

這裡的父組件不是指組件之間引用產生的父子關係,還是跟Vue.extend相關的父子關係。目前我也不太了解Vue.extend的相關內容,所以就不多說了。

vm._renderProxy

接著就是為組件實例的_renderProxy賦值。

//源碼位置備註:/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賦值的。

//源碼位置備註:/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方法內部實際上是利用ES6Proxy對象為將組件實例vm進行包裝,然後賦值給vm._renderProxy

關於Proxy的用法如下:

那我們簡單的寫一個關於Proxy的用法示例。

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的某個屬性時,如果屬性存在,則為其賦新值;如果不存在則列印'屬性不存在,無法賦值'
接著我們把上面這段程式碼放入瀏覽器的控制台運行,然後訪問代理對象的屬性:

然後在修改代理對象的屬性:


結果和我們前面描述一致。然後我們在說回initProxy,它實際上也就是在訪問vm上的某個屬性時做一些驗證,比如該屬性是否在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')  

我們把最後這幾個函數的調用順序和Vue官網的生命周期圖示對比一下:

可以發現程式碼和這個圖示基本上是一一對應的,所以_init方法被稱為是Vue實例的初始化方法。下面我們將逐個解讀_init內部按順序調用的那些方法。

initLifecycle-初始化生命周期

//源碼位置備註:/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-初始化事件

//源碼位置備註:/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表示的是什麼呢?我們寫一段程式碼驗證一下。

<!DOCTYPE html>  <html lang="en">  <head>      <meta charset="UTF-8">      <meta name="viewport" content="width=device-width, initial-scale=1.0">      <title>Vue的生命周期</title>      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>      <script>          var ChildComponent = Vue.component('child', {              mounted() {                  console.log(this);              },              methods: {                  triggerSelf(){                      console.log("triggerSelf");                  },                  triggerParent(){                      this.$emit('updateinfo');                  }              },              template: `<div id="child">                              <h3>這裡是子組件child</h3>                              <p>                                  <button v-on:click="triggerSelf">觸發本組件事件                                  </button>                              </p>                              <p>                              <button v-on:click="triggerParent">觸發父組件事件                              </button>                              </p>                          </div>`          })      </script>    </head>  <body>      <div id="app">          <h3>這裡是父組件App</h3>          <button v-on:click='destoryComponent'>銷毀組件</button>          <child v-on:updateinfo='updateInfo'>          </child>      </div>      <script>          var vm = new Vue({              el: '#app',              mounted() {                  console.log(this);              },              methods: {                  updateInfo: function() {                    },                  destoryComponent: function(){                    },              }          });      </script>  </body>  </html>  

我們將這段程式碼的邏輯簡單梳理一下。

首先是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屬性的列印結果。

從列印的結果可以看到,當前組件實例的_events屬性保存的只是父組件綁定在當前組件上的事件,而不是組件中所有的事件。

vm._hasHookEvent

_hasHookEvent屬性表示的是父組件是否通過v-hook:鉤子函數名稱把鉤子函數綁定到當前組件上。

updateComponentListeners(vm, listeners)

對於這個函數,我們首先需要關注的是listeners這個參數。我們看一下它是怎麼來的。

// init parent attached events  const listeners = vm.$options._parentListeners  

從注釋翻譯過來的意思就是初始化父組件添加的事件。到這裡不知道大家是否有和我相同的疑惑,我們前面說_events屬性保存的是父組件綁定在當前組件上的事件。這裡又說_parentListeners也是父組件添加的事件。這兩個屬性到底有什麼區別呢?
我們將上面的示例稍作修改,添加一條列印資訊(這裡只將修改的部分貼出來)

<script>  // 修改子組件child的mounted方法:列印屬性  var ChildComponent = Vue.component('child', {      mounted() {          console.log("this._events:");          console.log(this._events);          console.log("this.$options._parentListeners:");          console.log(this.$options._parentListeners);      },  })  </script>    <!--修改引用子組件的程式碼:增加兩個事件綁定(並且帶有事件修飾符) -->  <child v-on:updateinfo='updateInfo'         v-on:sayHello.once='sayHello'         v-on:SayBye.capture='SayBye'>  </child>    <script>  // 修改App組件的methods方法:增加兩個方法sayHello和sayBye  var vm = new Vue({      methods: {          sayHello: function(){            },          SayBye: function(){            },      }  });  </script>  

接著我們在瀏覽器中運行程式碼,查看結果。

從這個結果我們其實可以看到,_events_parentListeners保存的內容實際上都是父組件綁定在當前組件上的事件。只是保存的鍵值稍微有一些區別:

區別一:      前者事件名稱這個key直接是事件名稱      後者事件名稱這個key保存的是一個字元串和事件名稱的拼接,這個字元串是對修飾符的一個轉化(.once修飾符會轉化為~;.capture修飾符會轉化為!)  區別二:      前者事件名稱對應的value是一個數組,數組裡面才是對應的事件回調      後者事件名稱對應的vaule直接就是回調函數  

Ok,繼續我們的分析。

接著就是判斷這個listeners:假如listeners存在的話,就執行updateComponentListeners(vm, listeners)方法。我們看一下這個方法內部實現。

//源碼位置備註:/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是一個函數,函數內部邏輯程式碼為:

function add (event, fn) {    target.$on(event, fn)  }  

remove: remove也是一個函數,函數內部邏輯程式碼為:

function remove (event, fn) {    target.$off(event, fn)  }  

createOnceHandler

vm:這個參數就不用多說了,就是當前組件的實例。

這裡我們主要說一下add函數和remove函數中的兩個重要程式碼:target.$ontarget.$off

首先target是在event.js文件中定義的一個全局變數:

//源碼位置備註:/vue/src/core/instance/events.js  let target: any  

updateComponentListeners函數內部,我們能看到將組件實例賦值給了target

//源碼位置備註:/vue/src/core/instance/events.js  target = vm  

所以target就是組件實例。當然熟悉Vue的同學應該很快能反應上來$on$off方法本身就是定義在組件實例上和事件相關的方法。那組件實例上有關事件的方法除了$on$off方法之外,還有兩個方法:$once$emit

在這裡呢,我們暫時不詳細去解讀這四個事件方法的源碼實現,只截圖貼出Vue官網對這個四個實例方法的用法描述。

vm.$on

vm.$once

vm.$emit

vm.$emit的用法在 Vue父子組件通訊 一文中有詳細的示例。

vm.$off


updateListeners函數的參數基本解釋完了,接著我們在回歸到updateListeners函數的內部實現。

//源碼位置備註:/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對象為:

{      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-初始化模板

//源碼位置備註:/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


這是來自官網對vm.$slots的解釋,那為了方便,我還是寫一個示例。

<!DOCTYPE html>  <html lang="en">  <head>      <meta charset="UTF-8">      <meta name="viewport" content="width=device-width, initial-scale=1.0">      <title>Vue的生命周期</title>      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>      <script>          var ChildComponent = Vue.component('child', {              mounted() {                  console.log("Clild組件,this.$slots:");                  console.log(this.$slots);              },              template:'<div id="child">子組件Child</div>'          })      </script>    </head>  <body>      <div id="app">          <h1 slot='root'>App組件,slot='root'</h1>          <child>              <h3 slot='first'>這裡是slot=first</h3>              <h3 slot='first'>這裡是slot=first</h3>              <h3>這裡沒有設置slot</h3>              <h3 slot='last'>這裡是slot=last</h3>          </child>      </div>      <script>          var vm = new Vue({              el: '#app',              mounted() {                  console.log("App組件,this.$slots:");                  console.log(this.$slots);              }          });      </script>  </body>  </html>  

運行程式碼,看一下結果。

可以看到,child組件的vm.$slots列印結果是一個包含三個鍵值對的對象。其中keyfirst的值保存了兩個VNode對象,這兩個Vnode對象就是我們在引用child組件時寫的slot=first的兩個h3元素。那keylast的值也是同樣的道理。

keydefault的值保存了四個Vnode,其中有一個是引用child組件時寫沒有設置slot的那個h3元素,另外三個Vnode實際上是四個h3元素之間的換行,假如把child內部的h3這樣寫:

<child>      <h3 slot='first'>這裡是slot=first</h3><h3 slot='first'>這裡是slot=first</h3><h3>這裡沒有設置slot</h3><h3 slot='last'>這裡是slot=last</h3>  </child>  

那最終列印keydefault對應的值就只包含我們沒有設置sloth1元素。

所以源程式碼中的resolveSlots函數就是解析模板中父組件傳遞給當前組件的slot元素,並且轉化為Vnode賦值給當前組件實例的$slots對象。

vm.$scopeSlots

vm.$scopeSlotsVue中作用域插槽的內容,和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的用法。

<!DOCTYPE html>  <html lang="en">  <head>      <meta charset="UTF-8">      <meta name="viewport" content="width=device-width, initial-scale=1.0">      <title>Vue的生命周期</title>      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>      <script>          var ChildComponent = Vue.component('child', {              render:function(){                  return this.$createElement('p',this.$slots.first);              }          })      </script>    </head>  <body>      <div id="app">          <h1 slot='root'>App組件,slot='root'</h1>          <child>              <h3 slot='first'>這裡是slot=first</h3>              <h3 slot='first'>這裡是slot=first</h3>              <h3>這裡沒有設置slot</h3>              <h3 slot='last'>這裡是slot=last</h3>          </child>      </div>      <script>          var vm = new Vue({              el: '#app'          });      </script>  </body>  </html>  

這個示例程式碼和前面介紹vm.$slots的程式碼差不多,就是在創建子組件時編寫了render函數,並且使用了vm.$createElement返回模板的內容。那我們瀏覽器中的結果。

可以看到,正如我們所說,vm.$createElement$slotsfrist對應的 包含兩個Vnode對象的數組編譯成為兩個h3元素,並且放入第一個參數指定的p元素中,在經過子組件的render函數將vm.$createElement的返回值進行處理,就看到了瀏覽器中展示的效果。

vm.$createElement 內部實現暫時不深入探究,因為牽扯到VueVnode的內容,後面了解Vnode後在學習其內部實現。

vm.$attr和vm.$listener

這兩個屬性是有關組件通訊的實例屬性,賦值方式也非常簡單,不在多說。

callHook(beforeCreate)-調用生命周期鉤子函數

callhook函數執行的目的就是調用Vue的生命周期鉤子函數,函數的第二個參數是一個字元串,具體指定調用哪個鉤子函數。那在初始化階段,順序執行完 initLifecycleinitStateinitRender後就會調用beforeCreate鉤子函數。

接下來看下源碼實現。

//源碼位置備註:/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()  }  

首先根據鉤子函數的名稱從組件實例中獲取組件的鉤子函數,接著調用invokeWithErrorHandlinginvokeWithErrorHandling函數的第三個參數為null,所以invokeWithErrorHandling內部就是通過apply方法實現鉤子函數的調用。

我們應該看到源碼中是循環handlers然後調用invokeWithErrorHandling函數。那實際上,我們在編寫組件的時候是可以寫多個名稱相同的鉤子,但是實際上Vue在處理的時候只會在實例上保留最後一個重名的鉤子函數,那這個循環的意義何在呢?

為了求證,我在beforeCrated這個鉤子中列印了this.$options['before'],然後發現這個結果是一個數組,而且只有一個元素。

這樣想來就能理解這個循環的寫法了。

initInjections-初始化注入

initInjections這個函數是個Vue中的inject相關的內容。所以我們先看一下官方文檔度對inject的解釋

官方文檔中說injectprovide通常是一起使用的,它的作用實際上也是父子組件之間的通訊,但是會建議大家在開發高階組件時使用。

provide 是下文中initProvide的內容。

關於injectprovide的用法會有一個特點:只要父組件使用provide註冊了一個數據,那不管有多深的子組件嵌套,子組件中都能通過inject獲取到父組件上註冊的數據。

大致了解injectprovide的用法後,就能猜想到initInjections函數內部是如何處理inject的了:解析獲取當前組件中inject的值,需要查找父組件中的provide中是否註冊了某個值,如果有就返回,如果沒有則需要繼續向上查找父組件。
下面看一下initInjections函數的源碼實現。

// 源碼位置備註:/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,其核心程式碼如下:

// 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函數處理後會得到這樣的返回結果:

{      'name': '小土豆biubiubiu',      'age': 18,      'height': '180'  }  

最後在回到initInjections函數,後面的程式碼就是在非生產環境下,將inject中的數據變成響應式的,利用的也是雙向數據綁定的那一套原理。

initState-初始化狀態

//源碼位置備註:/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組件定義的一些屬性:propsmethodsdatacomputedWatch

我們主要看一下data數據的初始化,即initData函數的實現。

//源碼位置備註:/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的雙向數據綁定源碼實現] 裡面的Observer的實現和調用。

所以現在我們就知道將對象變得可觀測就是在Vue實例初始化階段的initData這一步中完成的。

initProvide-初始化

//源碼位置備註:/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)-調用生命周期鉤子函數

到這個階段已經順序執行完initLifecycleinitStateinitRendercallhook('beforeCreate')initInjectionsinitProvide這些方法,然後就會調用created鉤子函數。

callHook內部實現在前面已經說過,這裡也是一樣的,所以不再重複說明。

總結

到這裡,Vue2.x的生命周期的初始化階段就解讀完畢了。這裡我們將初始化階段做一個簡單的總結。

源碼還是很強大的,學習的過程還是比較艱難枯燥的,但是會發現很多有意思的寫法,還有我們經常看過的一些理論內容在源碼中的真實實踐,所以一定要堅持下去。期待下一篇文章[你還不知道Vue的生命周期嗎?帶你從Vue源碼了解Vue2.x的生命周期(模板編譯階段)]

作者:小土豆biubiubiu

部落格園:https://www.cnblogs.com/HouJiao/

掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d

簡書:https://www.jianshu.com/u/cb1c3884e6d5

微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術)

歡迎大家掃描微信二維碼進入群聊討論(若二維碼失效可添加微信JEmbrace拉你進群):


碼字不易,點贊鼓勵喲~