Vue原理解析(三):快速搞懂new Vue()時(shí)到底做了什么?(下)

上一篇:Vue原理解析(二):快速搞懂new Vue()時(shí)到底做了什么?(上)

讓我們繼續(xù)this._init()的初始化之旅,接下來(lái)又會(huì)執(zhí)行這樣的三個(gè)初始化方法:

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

5. initInjections(vm): 主要作用是初始化inject,可以訪問(wèn)到對(duì)應(yīng)的依賴。

injectprovide這里需要簡(jiǎn)單的提一下,這是vue@2.2版本添加的一對(duì)需要一起使用的API,它允許父級(jí)組件向它之后的所有子孫組件提供依賴,讓子孫組件無(wú)論嵌套多深都可以訪問(wèn)到,很cool有木有~

  • provide:提供一個(gè)對(duì)象或是返回一個(gè)對(duì)象的函數(shù)。
  • inject:是一個(gè)字符串?dāng)?shù)組或?qū)ο蟆?/li>

這一對(duì)APIvue官網(wǎng)有給出兩條食用提示:

provideinject 主要為高階插件/組件庫(kù)提供用例。并不推薦直接用于應(yīng)用程序代碼中。

  • 大概是因?yàn)闀?huì)讓組件數(shù)據(jù)層級(jí)關(guān)系變的混亂的緣故,但在開發(fā)組件庫(kù)時(shí)會(huì)很好使。

provideinject 綁定并不是可響應(yīng)的。這是刻意為之的。然而,如果你傳入了一個(gè)可監(jiān)聽的對(duì)象,那么其對(duì)象的屬性還是可響應(yīng)的。

  • 有個(gè)小技巧,這里可以將根組件data內(nèi)定義的屬性提供給子孫組件,這樣在不借助vuex的情況下就可以實(shí)現(xiàn)簡(jiǎn)單的全局狀態(tài)管理,還是很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!'
    }
  }
}

一但觸發(fā)handleClick事件之后,無(wú)論嵌套多深的子孫組件只要是使用了inject注入this.app.info變量的地方都會(huì)被響應(yīng),這就完成了簡(jiǎn)易的vuex。更多的示例大家可以去vue的官網(wǎng)翻閱,這里就不碼字了,現(xiàn)在我們來(lái)分析下這么cool的功能它究竟是怎么實(shí)現(xiàn)的~

雖然injectprovide是成對(duì)使用的,但是二者在內(nèi)部是分開初始化的。從上面三個(gè)初始化方法就能看出,先初始化inject,然后初始化props/data狀態(tài)相關(guān),最后初始化provide。這樣做的目的是可以在props/data中使用inject內(nèi)所注入的內(nèi)容。

我們首先來(lái)看一下初始化inject時(shí)的方法定義:

export function initInjections(vm) {
  const result = resolveInject(vm.$options.inject, vm) // 找結(jié)果
  
  ...
}

vm.$options.inject為之前合并后得到的用戶自定義的inject,然后使用resolveInject方法找到我們想要的結(jié)果,我們看下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后新增設(shè)置inject默認(rèn)參數(shù)相關(guān)邏輯
    }
    return result
  }
}

首先定義一個(gè)result返回找到的結(jié)果。接下來(lái)使用雙循環(huán)查找,外層的for循環(huán)會(huì)遍歷inject的每一項(xiàng),然后再內(nèi)層使用while循環(huán)自底向上的查找inject該項(xiàng)的父級(jí)是否有提供對(duì)應(yīng)的依賴。

Ps:這里可能有人會(huì)有疑問(wèn),之前inject的定義明明是數(shù)組,這里怎么可以通過(guò)Object.keys取值?這是因?yàn)樯弦徽略僮?code>options合并時(shí),也會(huì)對(duì)參數(shù)進(jìn)行格式化,如props的格式,定義為數(shù)組也會(huì)被轉(zhuǎn)為對(duì)象格式,inject被定義時(shí)是這樣的:

定義時(shí):
{
  inject: ['app']
}

格式化后:
{
  inject: {
    app: {
      from: 'app'
    }
  }
}

書接上文,source就是當(dāng)前的實(shí)例,而source._provided內(nèi)保存的就是當(dāng)前provide提供的值。首先從當(dāng)前實(shí)例查找,接著將它的父組件實(shí)例賦值給source,在它的父組件查找。找到后使用break跳出循環(huán),將搜索的結(jié)果賦值給result,接著查找下一個(gè)。

Ps:可能有人又會(huì)有疑問(wèn),這個(gè)時(shí)候是先初始化的inject再初始化的provide,怎么訪問(wèn)父級(jí)的provide了?它根本就沒初始化阿,這個(gè)時(shí)候我們就要再思考下了,因?yàn)?code>vue是組件式的,首先就會(huì)初始化父組件,然后才是初始化子組件,所以這個(gè)時(shí)候是有source._provided屬性的。

找到了想到的結(jié)果之后,我們補(bǔ)全之前initInjections的定義:

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

  if(result) { // 如果有結(jié)果
    toggleObserving(false)  // 刻意為之不被響應(yīng)式
    Object.keys(result).forEach(key => {
      ...
      defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}

如果有搜索結(jié)果,首先會(huì)調(diào)用toggleObserving(false),具體實(shí)現(xiàn)不用理會(huì),只用知道這個(gè)方法的作用是設(shè)置一個(gè)標(biāo)志位,將決定defineReactive()方法是否將它的第三個(gè)參數(shù)設(shè)置為響應(yīng)式數(shù)據(jù),也就是決定result[key]這個(gè)值是否會(huì)被設(shè)置為響應(yīng)式數(shù)據(jù),這里的參數(shù)為false,只是在vm下掛載key對(duì)應(yīng)普通的值,不過(guò)這樣就可以在當(dāng)前實(shí)例使用this訪問(wèn)到inject內(nèi)對(duì)應(yīng)的依賴項(xiàng)了,設(shè)置完畢之后再調(diào)用toggleObserving(true),改變標(biāo)志位,讓defineReactive()可以設(shè)置第三個(gè)參數(shù)為響應(yīng)式數(shù)據(jù)(defineReactive是響應(yīng)式原理很重要的方法,這里了解即可),也就是它該有的樣子。以上就是inject實(shí)現(xiàn)的相關(guān)原理,一句話來(lái)說(shuō)就是,首先遍歷每一項(xiàng),然后挨個(gè)遍歷每一項(xiàng)父級(jí)是否有依賴。

6. initState(vm): 初始化會(huì)被使用到的狀態(tài),狀態(tài)包括propsmethodsdatacomputedwatch五個(gè)選項(xiàng)。

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

現(xiàn)在這里的話只會(huì)介紹前面三類狀態(tài)的初始化做了什么,也就是propsmethodsdata,因?yàn)?code>computed和watch會(huì)涉及到響應(yīng)式相關(guān)的watcher,這里先略過(guò)。接下來(lái)我們依次有請(qǐng)這三位的初始化方法登場(chǎng):

6.1 initProps (vm, propsOptions):

  • 主要作用是檢測(cè)子組件接受的值是否符合規(guī)則,以及讓對(duì)應(yīng)的值可以用this直接訪問(wèn)。
function initProps(vm, propsOptions) {  // 第二個(gè)參數(shù)為驗(yàn)證規(guī)則
  const propsData = vm.$options.propsData || {}  // props具體的值
  const props = vm._props = {}  // 存放props
  const isRoot = !vm.$parent // 是否是根節(jié)點(diǎn)
  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內(nèi)的第二個(gè)參數(shù)propsOptions,就是當(dāng)前實(shí)例也就是通信角色里的子組件,它所定義的接受參數(shù)的規(guī)則。子組件的props規(guī)則是可以使用數(shù)組形式的定義的,不過(guò)再經(jīng)過(guò)合并options之后會(huì)被格式化為對(duì)象的形式:

定義時(shí):
{
  props: ['name', 'age']
}

格式化后:
{
  name: {
    type: null
  },
  age: {
    type: null
  }
}

所以在定義props規(guī)則時(shí),直接使用對(duì)象格式吧,這也是更好的書寫規(guī)范。

知道了規(guī)則之后,接下來(lái)需要知道父組件傳遞給子組件具體的值,它以對(duì)象的格式被放在vm.$options.propsData內(nèi),這也是合并options時(shí)得到的。接下來(lái)在實(shí)例下定義了一個(gè)空對(duì)象vm._props,它的作用是將符合規(guī)格的值掛載到它下面。isRoot的作用是判斷當(dāng)前組件是否是根組件,如果不是就不將props的轉(zhuǎn)為響應(yīng)式數(shù)據(jù)。

接下來(lái)遍歷格式化后的props驗(yàn)證規(guī)則,通過(guò)validateProp方法驗(yàn)證規(guī)則并得到相應(yīng)的值,將得到的值掛載到vm._props下。這個(gè)時(shí)候就可以通過(guò)this._props訪問(wèn)到props內(nèi)定義的值了:

props: ['name'],
methods: {
  handleClick() {
    console.log(this._props.name)
  }
}

不過(guò)直接訪問(wèn)內(nèi)部的私有變量這種方式并不友好,所以vue內(nèi)部做了一層代理,將對(duì)this.name的訪問(wèn)轉(zhuǎn)而為對(duì)this._props.name的訪問(wèn)。這里的proxy需要介紹下,因?yàn)橹蟮?code>data也會(huì)使用到,看下它的定義:

格式化了一下:
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
    }
  })
}

其實(shí)很簡(jiǎn)單,只是定義一個(gè)對(duì)象值的get方法,讀取時(shí)讓其返回另外的一個(gè)值,這里就完成了props的初始化。

6.2 initMethods (vm, methods):

  • 主要作用是將methods內(nèi)的方法掛載到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 的簡(jiǎn)寫
      warn(`只定義了key而沒有相應(yīng)的value`)
    }
    
    if(props && hasOwn(props, key)) {
      warn(`方法名和props的key重名了`)
    }
    
    if((key in vm) && isReserved(key)) {
      warn(`方法名已經(jīng)存在而且以_或$開頭`)
    }
    
    vm[key] = methods[key] == null
      ? noop  // 空函數(shù)
      : bind(methods[key], vm)  //  相當(dāng)于methods[key].bind(vm)
  }
}

methods的初始化相較而言就簡(jiǎn)單了很多。不過(guò)它也有很多邊界情況,如只定義了key而沒有方法具體的實(shí)現(xiàn)、keyprops重名了、key已經(jīng)存在且命名不規(guī)范,以_$開頭,至于為什么不行,我們第一章的時(shí)候有說(shuō)明了。最后將methods內(nèi)的方法掛載到this下,就完成了methods的初始化。

6.3 initData (vm):

  • 主要作用是初始化data,還是老套路,掛載到this下。有個(gè)重要的點(diǎn),之所以data內(nèi)的數(shù)據(jù)是響應(yīng)式的,是在這里初始化的,這個(gè)大家得有個(gè)印象~。
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm) // 通過(guò)data.call(vm, vm)得到返回的對(duì)象
    : data || {}
  if (!isPlainObject(data)) { // 如果不是一個(gè)對(duì)象格式
    data = {}
    warn(`data得是一個(gè)對(duì)象`)
  }
  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內(nèi)的方法重名了`)
    }
    
    if (props && hasOwn(props, key)) {
      warn(`和props內(nèi)的key重名了`)
    } else if (!isReserved(key)) { // key不能以_或$開頭
      proxy(vm, `_data`, key)
    }
  }
  observe(data, true)
}

首先通過(guò)vm.$options.data得到用戶定義的data,如果是function格式就執(zhí)行它,并返回執(zhí)行之后的結(jié)果,否則返回data{},將結(jié)果賦值給vm._data這個(gè)私有屬性。和props一樣的套路,最后用來(lái)做一層代理,如果得到的結(jié)果不是對(duì)象格式就是報(bào)錯(cuò)了。

然后遍歷data內(nèi)的每一項(xiàng),不能和methods以及props內(nèi)的key重名,然后使用proxy做一層代理。注意最后會(huì)執(zhí)行一個(gè)方法observe(data, true),它的作用了是遞歸的讓data內(nèi)的每一項(xiàng)數(shù)據(jù)都變成響應(yīng)式的。

其實(shí)不難發(fā)現(xiàn)它們仨主要做的事情差不多,首先不要相互之間有重名,然后可以被this直接訪問(wèn)到。

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

provide選項(xiàng)應(yīng)該是一個(gè)對(duì)象或是函數(shù),所以對(duì)它取值即可,就像取data內(nèi)的值類似,看下它的定義:

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

首先通過(guò)vm.$options.provide取得用戶定義的provide選項(xiàng),如果是一個(gè)function類型就執(zhí)行一下,得到返回后的結(jié)果,將其賦值給了vm._provided私有屬性,所以子組件在初始化inject時(shí)就可以訪問(wèn)到父組件提供的依賴了;如果不是function類型就直接返回定義的provide

8. callHook(vm, 'created'): 執(zhí)行用戶定義的created鉤子函數(shù),有mixin混入的也一并執(zhí)行。

終于我們?cè)竭^(guò)了created鉤子函數(shù),還是分別用一句話來(lái)介紹它們主要都干了什么事:

  • initInjections(vm):讓子組件inject的項(xiàng)可以訪問(wèn)到正確的值
  • initState(vm):將組件定義的狀態(tài)掛載到this下。
  • initProvide(vm):初始化父組件提供的provide依賴。
  • created:執(zhí)行組件的created鉤子函數(shù)

初始化的階段算是告一段落了,接下來(lái)我們會(huì)進(jìn)入組件的掛載階段。按照慣例我們還是以一道vue容易被問(wèn)道的面試題作為本章的結(jié)束吧~:

面試官微笑而又不失禮貌的問(wèn)道:

  • 請(qǐng)問(wèn)methods內(nèi)的方法可以使用箭頭函數(shù)么,會(huì)造成什么樣的結(jié)果?

懟回去:

  • 是不可以使用箭頭函數(shù)的,因?yàn)榧^函數(shù)的this是定義時(shí)就綁定的。在vue的內(nèi)部,methods內(nèi)每個(gè)方法的上下文是當(dāng)前的vm組件實(shí)例,methods[key].bind(vm),而如果使用使用箭頭函數(shù),函數(shù)的上下文就變成了父級(jí)的上下文,也就是undefined了,結(jié)果就是通過(guò)undefined訪問(wèn)任何變量都會(huì)報(bào)錯(cuò)。

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

手點(diǎn)個(gè)贊或關(guān)注唄,找起來(lái)也方便~

分享一個(gè)筆者自己寫的組件庫(kù),哪天可能會(huì)用的上了 ~ ↓

你可能會(huì)用的上的一個(gè)vue功能組件庫(kù),持續(xù)完善中...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容