Vue原理解析(二):初始化時beforeCreate以前作了什麼?

上一篇:Vue原理解析(一):Vue究竟是什麼?html

上一章節咱們知道了在new Vue()時,內部會執行一個this._init()方法,這個方法是在initMixin(Vue)內定義的:vue

export function initMixin(Vue) {
  Vue.prototype._init = function(options) {
    ...
  }
}
複製代碼

當執行new Vue()執行後,觸發的一系列初始化都在_init方法中啓動,它的實現以下:node

let uid = 0

Vue.prototype._init = function(options) {

  const vm = this
  vm._uid = uid++  // 惟一標識
  
  vm.$options = mergeOptions(  // 合併options
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
  ...
  initLifecycle(vm) // 開始一系列的初始化
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm)
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
複製代碼

先須要交代下,每個組件都是一個Vue構造函數的子類,這個以後會說明爲什麼如此。從上往下咱們一步步看,首先會定義_uid屬性,這是爲每一個組件每一次初始化時作的一個惟一的私有屬性標識,有時候會有些做用。面試

有一個使用它的小例子,找到一個組件全部的兄弟組件並剔除本身:vue-router

<div>
  ...
  <child-components />
  <child-components />  // 找到它的兄弟組件
  ... 其餘組件
  <child-components />
</div>
複製代碼

首先要找的組件須要定義name屬性,固然定義name屬性也是一個好的書寫習慣。首先經過本身的父組件($parent)的全部子組件($children)過濾出相同name集合的組件,這個時候他們就是同一個組件了,雖然它們name相同,可是_uid不一樣,最後在集合內根據_uid剔除掉本身便可。vuex

合併options配置

回到主線任務,接着會合並options並在實例上掛載一個$options屬性。合併什麼東西了?這裏是分兩種狀況的:數組

  1. 初始化new Vue

在執行new Vue構造函數時,參數就是一個對象,也就是用戶的自定義配置;會將它和vue以前定義的原型方法,全局API屬性;還有全局的Vue.mixin內的參數,將這些都合併成爲一個新的options,最後賦值給一個的新的屬性$options瀏覽器

  1. 子組件初始化

若是是子組件初始化,除了合併以上那些外,還會將父組件的參數進行合併,若有父組件定義在子組件上的eventprops等等。bash

通過合併以後就能夠經過this.$options.data訪問到用戶定義的data函數,this.$options.name訪問到用戶定義的組件名稱,這個合併後的屬性很重要,會被常用到。app

接下里會順序的執行一堆初始化方法,首先是這三個:

1. initLifecycle(vm)
2. initEvents(vm)
3. initRender(vm)
複製代碼

1. initLifecycle(vm): 主要做用是確認組件的父子關係和初始化某些實例屬性。

export function initLifecycle(vm) {
  const options = vm.$options  // 以前合併的屬性
  
  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  // 讓每個子組件的$root屬性都是根組件
  
  vm.$children = []
  vm.$refs = {}
  
  vm._watcher = null
  ...
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
複製代碼

vue是組件式開發的,因此當前實例可能會是其餘組件的子組件的同時也多是其餘組件的父組件。

首先會找到當前組件第一個非抽象類型的父組件,因此若是當前組件有父級且當前組件不是抽象組件就一直向上查找,直至找到後將找到的父級賦值給實例屬性vm.$parent,而後將當前實例push到找到的父級的$children實例屬性內,從而創建組件的父子關係。接下來的一些_開頭是私有實例屬性咱們記住是在這裏定義的便可,具體意思也是之後用到的時候再作說明。

2. initEvents(vm): 主要做用是將父組件在使用v-on@註冊的自定義事件添加到子組件的事件中心中。

首先看下這個方法定義的地方:

export function initEvents (vm) {
  vm._events = Object.create(null)  // 事件中心
  ...
  const listeners = vm.$options._parentListeners  // 通過合併options獲得的
  if (listeners) {
    updateComponentListeners(vm, listeners) 
  }
}
複製代碼

咱們首先要知道在vue中事件分爲兩種,他們的處理方式也各有不一樣:

2.1 原生事件

在執行initEvents以前的模板編譯階段,會判斷遇到的是html標籤仍是組件名,若是是html標籤會在轉爲真實dom以後使用addEventListener註冊瀏覽器原生事件。綁定事件是掛載dom的最後階段,這裏只是初始化階段,這裏主要是處理自定義事件相關,也就是另一種,這裏聲明下,你們不要理會錯了執行順序。

2.2 自定義事件

在經歷過合併options階段後,子組件就能夠從vm.$options._parentListeners讀取到父組件傳過來的自定義事件:

<child-components @select='handleSelect' />
複製代碼

傳過來的事件數據格式是{select:function(){}}這樣的,在initEvents方法內定義vm._events用來存儲傳過來的事件集合。

內部執行的方法updateComponentListeners(vm, listeners)主要是執行updateListeners方法。這個方法有兩個執行時機,首先是如今的初始化階段,還一個就是最後patch時的原生事件也會用到。它的做用是比較新舊事件的列表來肯定事件的添加和移除以及事件修飾符的處理,如今主要看自定義事件的添加,它的做用是藉助以前定義的$on$emit方法,完成父子組件事件的通訊,(詳細的原理說明會在以後的全局API章節統一說明)。首先使用$onvm.events事件中心下建立一個自定義事件名的數組集合項,數組內的每一項都是對應事件名的回調函數,例如:

vm._events.select = [function handleSelect(){}, ...]  // 能夠有多個
複製代碼

註冊完成以後,使用$emit方法執行事件:

this.$emit('select')
複製代碼

首先會讀取到事件中心內$emit方法第一個參數select的對象的數組集合,而後將數組內每一個回調函數順序執行一遍即完成了$emit作的事情。

不知道你們有沒有注意到this.$emit這個方法是在當前組件實例觸發的,因此事件的原理可能跟大部分人理解的不同,並非父組件監聽,子組件往父組件去派發事件。

而是子組件往自身的實例上派發事件,只是由於回調函數是在父組件的做用域下定義的,因此執行了父組件內定義的方法,就形成了父子之間事件通訊的假象。知道這個原理特性後,咱們能夠作一些更cool的事情,例如:

<div>
  <parent-component>  // $on添加事件
    <child-component-1>
      <child-component-2>
        <child-component-3 />  // $emit觸發事件
      </child-component-2>
    </child-components-1>
  </parent-component>
</div>
複製代碼

咱們可不能夠在parent-component內使用$on添加事件到當前實例的事件中心,而在child-components-3內找到parent-component的組件實例並在它的事件中心觸發對應的事件實現跨組件通訊了,答案是能夠了!這一原理髮現再開發組件庫時會有必定幫助。

3. initRender(vm): 主要做用是掛載能夠將render函數轉爲vnode的方法。

export function initRender(vm) {
  vm._vnode = null
  ...
  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)  // 轉化手寫的
  ...
}
複製代碼

主要做用是掛載vm._cvm.$createElement兩個方法,它們只是最後一個參數不一樣,這兩個方法均可以將render函數轉爲vnode,從命名你們應該能夠看出區別,vm._c轉換的是經過編譯器將template轉換而來的render函數;而vm.$createElement轉換的是用戶自定義的render函數,好比:

new Vue({
  data: {
    msg: 'hello Vue!'
  },
  render(h) { // 這裏的 h 就是vm.$createElement
    return h('span', this.msg);  
  }
}).$mount('#app');
複製代碼

render函數的參數h就是vm.$createElement方法,將內部定義的樹形結構數據轉爲Vnode的實例。

4. callHook(vm, 'beforeCreate')

終於咱們要執行實例的第一個生命週期鉤子beforeCreate,這裏callHook的原理是怎樣的,咱們以後的生命週期章節會說明,如今這裏只須要知道它會執行用戶自定義的生命週期方法,若是有mixin混入的也一併執行。

好吧,實例的第一個生命週期鉤子階段的初始化工做完成了,一句話來主要說明下他們作了什麼事情:

  • initLifecycle(vm):確認組件(也是vue實例)的父子關係
  • initEvents(vm):將父組件的自定義事件傳遞給子組件
  • initRender(vm):提供將render函數轉爲vnode的方法
  • beforeCreate:執行組件的beforeCreate鉤子函數

最後仍是以一道vue容易被問道的面試題做爲本章節的結束吧:

面試官微笑而又不失禮貌的問道:

  • 請問能夠在beforeCreate鉤子內經過this訪問到data中定義的變量麼,爲何以及請問這個鉤子能夠作什麼?

懟回去:

  • 是不能夠訪問的,由於在vue初始化階段,這個時候data中的變量尚未被掛載到this上,這個時候訪問值會是undefinedbeforeCreate這個鉤子在平時業務開發中用的比較少,而像插件內部的instanll方法經過Vue.use方法安裝時通常會選在beforeCreate這個鉤子內執行,vue-routervuex就是這麼幹的。

下一篇:Vue原理解析(三):初始化時created以前作了什麼?

順手點個贊或關注唄,找起來也方便~

參考:

Vue.js源碼全方位深刻解析

Vue.js深刻淺出

Vue.js組件精講

剖析 Vue.js 內部運行機制

相關文章
相關標籤/搜索