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

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

我們繼續this._init() 的初始化相關操作, 接着又會執行如下三個初始化方法:

initInjections(vm)
initState(vm)
initProvide(vm)

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

injectprovide 這裏需要簡單的提一下, 這是[email protected] 版本添加的一對需要一起使用的API, 它 允許父級組件向它之後的子孫組件提供依賴,讓子孫組件無論嵌套多深都可以訪問到.

  • provide : 提供一個對象或是返回一個對象的函數。
  • inject : 是一個字符串數組或對象。

這一對APIVue 官網有給出兩條實用提示:

provideinject 主要爲高階插件/組件庫提供用例。 並不推薦直接用於應用程序代碼中。

  • 大概是因爲會讓組件數據層級關係變的混亂的緣故, 但在開發組件庫時會很好使。

provideinject 綁定並不是可響應的。 這是刻意爲之的。 然而,如果你傳入一個可以監聽的對象,那麼其對象的屬性還是可響應的。

  • 有個小技巧, 這裏可以將根組件data 內定義的屬性提供給子孫組件, 這樣在不借助vuex 的情況下就可以實現簡單的全局狀態管理。
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。 接下來我們來看下這功能究竟怎麼實現的~

雖然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
      }
    ... [email protected]後新增設置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): 初始化會被使用到的狀態, 狀態包括: ==props, methods, data, computed, watch == 五個選項。

先看下 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)
  }
}

現在這裏的話只介紹前面三類狀態的初始化做了什麼, 也就是props, methods, data, 因爲computed 和 watch == 會涉及到響應式相關的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 鉤子函數。

初始化階段算是告一段落了, 接下來我們會進入組件的掛載階段。
本章還是以一個問題來結尾:

  • 請問methods 內的方法可以使用箭頭函數麼? 會產生怎樣的結果?
    解答:

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

下一篇: Vue原理解釋(四): 虛擬Dom是怎麼生成的 (上)

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