Vue 原理解析(二):初始化時beforeCreate之前做了什麼

上一篇: Vue原理解析(一) Vue到底是什麼

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

export function initMixin(Vue) {
  Vue.prototype._init = function(options) {
    ...
  }
}

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

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== 屬性, 這是爲每個組件每一次初始化時做的一個唯一的私有屬性標識,有時候會有些作用。
舉個小例子(找到一個組件所有的兄弟組件並剔除自己):

<div>
  ...
  <child-components />
  <child-components />  // 找到它的兄弟組件
  ... 其他組件
  <child-components />
</div>

首先需要找到組件定義的name 屬性, 首先通過自己的父組件==(parent)==(parent)==的所有子組件(children) 過濾出相同name 集合的組件,這時他們就是同一個組件了,雖然name 相同,但是==_uid== 不同, 最後在集合內根據==_uid== 剔除掉自己即可。

合併options配置

回到主線任務,接着會合並options 並在實例上掛載一個==$options==屬性。 這裏合併分兩種情況:
1. 初始化new Vue

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

2. 子組件初始化

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

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

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

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方法, 完成父子組件事件的通信。 首先使用 $on 往 vm.events 事件中心下創建一個自定義事件名的數組集合項, 數組內的每一項都是對應事件名的回調函數。例如:

vm._events.select = [function handleSelect(){}, ...]  // 可以有多個

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

this.$emit('select')

首先會讀取事件中心內==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-cmponents-3 內找到 parent-componet 的組件實例並在它的事件中心出發對應的事件實現誇組件通信呢? 答案是可以的! 這一原理髮現在開發組件庫時會有一定幫助。

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._c 和 vm.createElement==render====vnode==,==vm.c====template====render==vm.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 鉤子函數

最後已已一個問題來結尾本章:

  • 請問可以在beforeCreate 鉤子內通過 this 訪問到data中定義的變量麼,爲什麼? 請問這個鉤子可以做什麼?

解答:

  • 是不可以訪問的。 因爲在vue 初始化階段, 這個時候data 中的變量還沒有掛載到this 上, 這個時候訪問值會是 undefinedbeforeCreate 這個鉤子在平時業務開發中比較少用。 像插件內部install 方法通過, Vue.use 方法安裝時一般會選在beforeCreate 這個鉤子內執行, vue-routervuex 就是這麼幹的。

下一篇: vue 原理解析(三): 初始化時 created 之前做了什麼?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章