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

飛躍瘋人院發表於2019-07-24

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

讓我們繼續this._init()的初始化之旅,接下來又會執行這樣的三個初始化方法:

initInjections(vm)
initState(vm)
initProvide(vm)
複製程式碼

5. initInjections(vm): 主要作用是初始化inject,可以訪問到對應的依賴。

injectprovide這裡需要簡單的提一下,這是vue@2.2版本新增的一對需要一起使用的API,它允許父級元件向它之後的所有子孫元件提供依賴,讓子孫元件無論巢狀多深都可以訪問到,很cool有木有~

  • provide:提供一個物件或是返回一個物件的函式。
  • inject:是一個字串陣列或物件。

這一對APIvue官網有給出兩條食用提示:

provideinject 主要為高階外掛/元件庫提供用例。並不推薦直接用於應用程式程式碼中。

  • 大概是因為會讓元件資料層級關係變的混亂的緣故,但在開發元件庫時會很好使。

provideinject 繫結並不是可響應的。這是刻意為之的。然而,如果你傳入了一個可監聽的物件,那麼其物件的屬性還是可響應的。

  • 有個小技巧,這裡可以將根元件data內定義的屬性提供給子孫元件,這樣在不借助vuex的情況下就可以實現簡單的全域性狀態管理,還是很cool的~
app.vue 根元件

export default {
  provide() {
    return {
      app: this
    }
  },
  data() {
    return {
      info: 'hello world!'
    }
  }
}

child.vue 子孫元件

export default {
  inject: ['app'],
  methods: {
    handleClick() {
      this.app.info = 'hello vue!'
    }
  }
}
複製程式碼

一但觸發handleClick事件之後,無論巢狀多深的子孫元件只要是使用了inject注入this.app.info變數的地方都會被響應,這就完成了簡易的vuex。更多的示例大家可以去vue的官網翻閱,這裡就不碼字了,現在我們來分析下這麼cool的功能它究竟是怎麼實現的~

雖然injectprovide是成對使用的,但是二者在內部是分開初始化的。從上面三個初始化方法就能看出,先初始化inject,然後初始化props/data狀態相關,最後初始化provide。這樣做的目的是可以在props/data中使用inject內所注入的內容。

我們首先來看一下初始化inject時的方法定義:

export function initInjections(vm) {
  const result = resolveInject(vm.$options.inject, vm) // 找結果
  
  ...
}
複製程式碼

vm.$options.inject為之前合併後得到的使用者自定義的inject,然後使用resolveInject方法找到我們想要的結果,我們看下resolveInject方法的定義:

export function resolveInject (inject, vm) {
  if (inject) {
    const result = Object.create(null)
    const keys = Object.keys(inject)  //省略Symbol情況

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      const provideKey = inject[key].from
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) { //hasOwn為是否有
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
    ... vue@2.5後新增設定inject預設引數相關邏輯
    }
    return result
  }
}
複製程式碼

首先定義一個result返回找到的結果。接下來使用雙迴圈查詢,外層的for迴圈會遍歷inject的每一項,然後再內層使用while迴圈自底向上的查詢inject該項的父級是否有提供對應的依賴。

Ps:這裡可能有人會有疑問,之前inject的定義明明是陣列,這裡怎麼可以通過Object.keys取值?這是因為上一章再做options合併時,也會對引數進行格式化,如props的格式,定義為陣列也會被轉為物件格式,inject被定義時是這樣的:

定義時:
{
  inject: ['app']
}

格式化後:
{
  inject: {
    app: {
      from: 'app'
    }
  }
}
複製程式碼

書接上文,source就是當前的例項,而source._provided內儲存的就是當前provide提供的值。首先從當前例項查詢,接著將它的父元件例項賦值給source,在它的父元件查詢。找到後使用break跳出迴圈,將搜尋的結果賦值給result,接著查詢下一個。

Ps:可能有人又會有疑問,這個時候是先初始化的inject再初始化的provide,怎麼訪問父級的provide了?它根本就沒初始化阿,這個時候我們就要再思考下了,因為vue是元件式的,首先就會初始化父元件,然後才是初始化子元件,所以這個時候是有source._provided屬性的。

找到了想到的結果之後,我們補全之前initInjections的定義:

export function initInjections(vm) {
  const result = resolveInject(vm.$options.inject, vm)

  if(result) { // 如果有結果
    toggleObserving(false)  // 刻意為之不被響應式
    Object.keys(result).forEach(key => {
      ...
      defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}
複製程式碼

如果有搜尋結果,首先會呼叫toggleObserving(false),具體實現不用理會,只用知道這個方法的作用是設定一個標誌位,將決定defineReactive()方法是否將它的第三個引數設定為響應式資料,也就是決定result[key]這個值是否會被設定為響應式資料,這裡的引數為false,只是在vm下掛載key對應普通的值,不過這樣就可以在當前例項使用this訪問到inject內對應的依賴項了,設定完畢之後再呼叫toggleObserving(true),改變標誌位,讓defineReactive()可以設定第三個引數為響應式資料(defineReactive是響應式原理很重要的方法,這裡瞭解即可),也就是它該有的樣子。以上就是inject實現的相關原理,一句話來說就是,首先遍歷每一項,然後挨個遍歷每一項父級是否有依賴。

6. initState(vm): 初始化會被使用到的狀態,狀態包括propsmethodsdatacomputedwatch五個選項。

首先看下initState(vm)方法的定義:

export function initState(vm) {
  ...
  const opts = vm.$options
  if(opts.props) initProps(vm, opts.props)
  if(opts.methods) initMethods(vm, opts.methods)
  if(opts.data) initData(vm)
  ...
  if(opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製程式碼

現在這裡的話只會介紹前面三類狀態的初始化做了什麼,也就是propsmethodsdata,因為computedwatch會涉及到響應式相關的watcher,這裡先略過。接下來我們依次有請這三位的初始化方法登場:

6.1 initProps (vm, propsOptions):

  • 主要作用是檢測子元件接受的值是否符合規則,以及讓對應的值可以用this直接訪問。
function initProps(vm, propsOptions) {  // 第二個引數為驗證規則
  const propsData = vm.$options.propsData || {}  // props具體的值
  const props = vm._props = {}  // 存放props
  const isRoot = !vm.$parent // 是否是根節點
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    const value = validateProp(key, propsOptions, propsData, vm)
    defineReactive(props, key, value)
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}
複製程式碼

我們知道props是作為父元件向子元件通訊的重要方式,而initProps內的第二個引數propsOptions,就是當前例項也就是通訊角色裡的子元件,它所定義的接受引數的規則。子元件的props規則是可以使用陣列形式的定義的,不過再經過合併options之後會被格式化為物件的形式:

定義時:
{
  props: ['name', 'age']
}

格式化後:
{
  name: {
    type: null
  },
  age: {
    type: null
  }
}
複製程式碼

所以在定義props規則時,直接使用物件格式吧,這也是更好的書寫規範。

知道了規則之後,接下來需要知道父元件傳遞給子元件具體的值,它以物件的格式被放在vm.$options.propsData內,這也是合併options時得到的。接下來在例項下定義了一個空物件vm._props,它的作用是將符合規格的值掛載到它下面。isRoot的作用是判斷當前元件是否是根元件,如果不是就不將props的轉為響應式資料。

接下來遍歷格式化後的props驗證規則,通過validateProp方法驗證規則並得到相應的值,將得到的值掛載到vm._props下。這個時候就可以通過this._props訪問到props內定義的值了:

props: ['name'],
methods: {
  handleClick() {
    console.log(this._props.name)
  }
}
複製程式碼

不過直接訪問內部的私有變數這種方式並不友好,所以vue內部做了一層代理,將對this.name的訪問轉而為對this._props.name的訪問。這裡的proxy需要介紹下,因為之後的data也會使用到,看下它的定義:

格式化了一下:
export function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      return this[sourceKey][key]
    },
    set: function () {
      this[sourceKey][key] = val
    }
  })
}
複製程式碼

其實很簡單,只是定義一個物件值的get方法,讀取時讓其返回另外的一個值,這裡就完成了props的初始化。

6.2 initMethods (vm, methods):

  • 主要作用是將methods內的方法掛載到this下。
function initMethods(vm, methods) {
  const props = vm.$options.props
  for(const key in methods) {
    
    if(methods[key] == null) {  // methods[key] === null || methods[key] === undefined 的簡寫
      warn(`只定義了key而沒有相應的value`)
    }
    
    if(props && hasOwn(props, key)) {
      warn(`方法名和props的key重名了`)
    }
    
    if((key in vm) && isReserved(key)) {
      warn(`方法名已經存在而且以_或$開頭`)
    }
    
    vm[key] = methods[key] == null
      ? noop  // 空函式
      : bind(methods[key], vm)  //  相當於methods[key].bind(vm)
  }
}
複製程式碼

methods的初始化相較而言就簡單了很多。不過它也有很多邊界情況,如只定義了key而沒有方法具體的實現、keyprops重名了、key已經存在且命名不規範,以_$開頭,至於為什麼不行,我們第一章的時候有說明了。最後將methods內的方法掛載到this下,就完成了methods的初始化。

6.3 initData (vm):

  • 主要作用是初始化data,還是老套路,掛載到this下。有個重要的點,之所以data內的資料是響應式的,是在這裡初始化的,這個大家得有個印象~。
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm) // 通過data.call(vm, vm)得到返回的物件
    : data || {}
  if (!isPlainObject(data)) { // 如果不是一個物件格式
    data = {}
    warn(`data得是一個物件`)
  }
  const keys = Object.keys(data)
  const props = vm.$options.props  // 得到props
  const methods = vm.$options.methods  // 得到methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (methods && hasOwn(methods, key)) {
      warn(`和methods內的方法重名了`)
    }
    
    if (props && hasOwn(props, key)) {
      warn(`和props內的key重名了`)
    } else if (!isReserved(key)) { // key不能以_或$開頭
      proxy(vm, `_data`, key)
    }
  }
  observe(data, true)
}
複製程式碼

首先通過vm.$options.data得到使用者定義的data,如果是function格式就執行它,並返回執行之後的結果,否則返回data{},將結果賦值給vm._data這個私有屬性。和props一樣的套路,最後用來做一層代理,如果得到的結果不是物件格式就是報錯了。

然後遍歷data內的每一項,不能和methods以及props內的key重名,然後使用proxy做一層代理。注意最後會執行一個方法observe(data, true),它的作用了是遞迴的讓data內的每一項資料都變成響應式的。

其實不難發現它們仨主要做的事情差不多,首先不要相互之間有重名,然後可以被this直接訪問到。

7. initProvide(vm): 主要作用是初始化provide為子元件提供依賴。

provide選項應該是一個物件或是函式,所以對它取值即可,就像取data內的值類似,看下它的定義:

export function initProvide (vm) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}
複製程式碼

首先通過vm.$options.provide取得使用者定義的provide選項,如果是一個function型別就執行一下,得到返回後的結果,將其賦值給了vm._provided私有屬性,所以子元件在初始化inject時就可以訪問到父元件提供的依賴了;如果不是function型別就直接返回定義的provide

8. callHook(vm, 'created'): 執行使用者定義的created鉤子函式,有mixin混入的也一併執行。

終於我們越過了created鉤子函式,還是分別用一句話來介紹它們主要都幹了什麼事:

  • initInjections(vm):讓子元件inject的項可以訪問到正確的值
  • initState(vm):將元件定義的狀態掛載到this下。
  • initProvide(vm):初始化父元件提供的provide依賴。
  • created:執行元件的created鉤子函式

初始化的階段算是告一段落了,接下來我們會進入元件的掛載階段。按照慣例我們還是以一道vue容易被問道的面試題作為本章的結束吧~:

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

  • 請問methods內的方法可以使用箭頭函式麼,會造成什麼樣的結果?

懟回去:

  • 是不可以使用箭頭函式的,因為箭頭函式的this是定義時就繫結的。在vue的內部,methods內每個方法的上下文是當前的vm元件例項,methods[key].bind(vm),而如果使用使用箭頭函式,函式的上下文就變成了父級的上下文,也就是undefined了,結果就是通過undefined訪問任何變數都會報錯。

下一篇: Vue原理解析(四):你知道被大家聊爛了的虛擬Dom是怎麼生成的嗎?(上)

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

參考:

Vue.js原始碼全方位深入解析

Vue.js深入淺出

Vue.js元件精講

分享一個元件庫給大家,可能會用的上 ~ ↓

你可能會用的上的一個vue功能元件庫,持續完善中...

相關文章