Vue2.0 源碼分析筆記(六)props、methods、provide、inject初始化過程

props初始化過程

initprops 方法定義在src/instance/state.js中

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

以上代碼簡(jiǎn)寫為

if (!isRoot) {
  toggleObserving(false)
}
for (const key in propsOptions) {
  // 省略...
  if (process.env.NODE_ENV !== 'production') {
    // 省略...
  } else {
    defineReactive(props, key, value)
  }
  // 省略...
}
toggleObserving(true)

為了搞清楚其目的,我們需要找到 defineReactive 函數(shù),注意如下高亮的代碼:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 省略...
//==============高亮=========
  let childOb = !shallow && observe(val)  
//==============高亮=========
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 省略...
    },
    set: function reactiveSetter (newVal) {
      // 省略...
    }
  })
}

如上那句高亮的代碼所示,在使用 defineReactive 函數(shù)定義屬性時(shí),會(huì)調(diào)用 observe 函數(shù)對(duì)值繼續(xù)進(jìn)行觀測(cè)。但由于之前使用了 toggleObserving(false) 函數(shù)關(guān)閉了開關(guān),所以上面高亮代碼中調(diào)用 observe 函數(shù)是一個(gè)無效調(diào)用。所以我們可以得出一個(gè)結(jié)論:在定義 props 數(shù)據(jù)時(shí),不將 prop 值轉(zhuǎn)換為響應(yīng)式數(shù)據(jù),這里要注意的是:由于 props 本身是通過 defineReactive 定義的,所以 props 本身是響應(yīng)式的,但沒有對(duì)值進(jìn)行深度定義。為什么這樣做呢?很簡(jiǎn)單,我們知道 props 是來自外界的數(shù)據(jù),或者更具體一點(diǎn)的說,props 是來自父組件的數(shù)據(jù),這個(gè)數(shù)據(jù)如果是一個(gè)對(duì)象(包括純對(duì)象和數(shù)組),那么它本身可能已經(jīng)是響應(yīng)式的了,所以不再需要重復(fù)定義。另外在定義 props 數(shù)據(jù)之后,又調(diào)用 toggleObserving(true) 函數(shù)將開關(guān)開啟,這么做的目的是不影響后續(xù)代碼的功能,因?yàn)檫@個(gè)開關(guān)是全局的。
最后大家還要注意一點(diǎn),如下:

if (!isRoot) {
  toggleObserving(false)
}

這段代碼說明,只有當(dāng)不是根組件的時(shí)候才會(huì)關(guān)閉開關(guān),這說明如果當(dāng)前組件實(shí)例是根組件的話,那么定義的 props 的值也會(huì)被定義為響應(yīng)式數(shù)據(jù)。

props 的校驗(yàn)

const value = validateProp(key, propsOptions, propsData, vm)

export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // boolean casting
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    // skip validation for weex recycle-list child component props
    !(__WEEX__ && isObject(value) && ('@binding' in value))
  ) {
    assertProp(prop, key, value, vm, absent)
  }
  return value
}

validateProp 一開始并沒有對(duì) props 的類型做校驗(yàn),首先如果一個(gè) prop 的類型是布爾類型,則為其設(shè)置合理的布爾值,其次又調(diào)用了 getPropDefaultValue 函數(shù)獲取 prop 的默認(rèn)值,而如上這段代碼才是真正用來對(duì) props 的類型做校驗(yàn)的。通過如上 if 語句的條件可知,僅在非生產(chǎn)環(huán)境下才會(huì)對(duì) props 做類型校驗(yàn),另外還有一個(gè)條件是用來跳過 weex 環(huán)境下某種條件的判斷的,我們不做講解??傊嬲男r?yàn)工作是由 assertProp 函數(shù)完成的。

methods 選項(xiàng)的初始化及實(shí)現(xiàn)

methods 選項(xiàng)實(shí)現(xiàn)要簡(jiǎn)單的多,打開 src/core/instance/state.js 文件找到 initMethods 函數(shù),如下:

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (methods[key] == null) {
        warn(
          `Method "${key}" has an undefined value in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}

這樣一來可以很清晰的看到 methods 選項(xiàng)是如何實(shí)現(xiàn)的,就是通過 for...in 循環(huán)遍歷 methods 選項(xiàng)對(duì)象,其中 key 就是每個(gè)方法的名字。最關(guān)鍵的是循環(huán)的最后一句代碼:

vm[key] = methods[key] == null ? noop : bind(methods[key], vm)

通過這句代碼可知,之所以能夠通過組件實(shí)例對(duì)象訪問 methods 選項(xiàng)中定義的方法,就是因?yàn)樵诮M件實(shí)例對(duì)象上定義了與 methods 選項(xiàng)中所定義的同名方法,當(dāng)然了在定義到組件實(shí)例對(duì)象之前要檢測(cè)該方法是否真正的有定義:methods[key] == null,如果沒有則添加一個(gè)空函數(shù)到組件實(shí)例對(duì)象上。

provide 選項(xiàng)的初始化及實(shí)現(xiàn)

Vue.prototype._init 方法中的一段用來完成初始化工作的代碼:

initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

可以發(fā)現(xiàn) initInjections 函數(shù)在 initProvide 函數(shù)之前被調(diào)用,這說明對(duì)于任何一個(gè)組件來講,總是要優(yōu)先初始化 inject 選項(xiàng),再初始化 provide 選項(xiàng),這么做是有原因的,我們后面會(huì)提到。但是我們知道 inject 選項(xiàng)的數(shù)據(jù)需要從父代組件中的 provide 獲取,所以我們優(yōu)先來了解 provide 選項(xiàng)的實(shí)現(xiàn),然后再查看 inject 選項(xiàng)的實(shí)現(xiàn)。
打開 src/core/instance/inject.js 文件,找到 initProvide 函數(shù),如下:

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

如上是 initProvide 函數(shù)的全部代碼,它接收組件實(shí)例對(duì)象作為參數(shù)。在 initProvide 函數(shù)內(nèi)部首先定義了 provide 常量,它的值是 vm.$options.provide 選項(xiàng)的引用,接著是一個(gè) if 條件語句,只有在 provide 選項(xiàng)存在的情況下才會(huì)執(zhí)行 if 語句塊內(nèi)的代碼,我們知道 provide 選項(xiàng)可以是對(duì)象,也可以是一個(gè)返回對(duì)象的函數(shù)。所以在 if 語句塊內(nèi)使用 typeof 操作符檢測(cè) provide 常量的類型,如果是函數(shù)則執(zhí)行該函數(shù)獲取數(shù)據(jù),否則直接將 provide 本身作為數(shù)據(jù)。最后將數(shù)據(jù)復(fù)制給組件實(shí)例對(duì)象的 vm._provided 屬性,后面我們可以看到當(dāng)組件初始化 inject 選項(xiàng)時(shí),其注入的數(shù)據(jù)就是從父代組件實(shí)例的 vm._provided 屬性中獲取的。

以上就是 provide 選項(xiàng)的初始化及實(shí)現(xiàn),它本質(zhì)上就是在組件實(shí)例對(duì)象上添加了 vm._provided 屬性,并保存了用于子代組件的數(shù)據(jù)。

inject 選項(xiàng)的初始化及實(shí)現(xiàn)

看完了 provide 選項(xiàng)的初始化及實(shí)現(xiàn),接下來我們研究一下 inject 選項(xiàng)的初始化及實(shí)現(xiàn)。找到 initInjections 函數(shù),它也定義在 src/core/instance/inject.js 文件,如下是 initInjections 函數(shù)的整體結(jié)構(gòu):

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    // 省略...
  }
}

找到 resolveInject 函數(shù),它定義在 initInjections 函數(shù)的下方,如下是其函數(shù)簽名:

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // #6574 in case the inject object is observed...
      if (key === '__ob__') continue
      const provideKey = inject[key].from
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      if (!source) {
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }
}

keys 常量中保存 inject 選項(xiàng)對(duì)象的每一個(gè)鍵名,接下來的代碼使用 for 循環(huán),用來遍歷剛剛獲取到的 keys 數(shù)組,其中 key 常量就是 keys 數(shù)組中的每一個(gè)值,即 inject 選項(xiàng)的每一個(gè)鍵值,provideKey 常量保存的是每一個(gè) inject 選項(xiàng)內(nèi)所定義的注入對(duì)象的 from 屬性的值,我們知道 from 屬性的值代表著 vm._provided 數(shù)據(jù)中的每個(gè)數(shù)據(jù)的鍵名,所以 provideKey 常量將用來查找所注入的數(shù)據(jù)。最后定義了 source 變量,它的初始值是當(dāng)前組件實(shí)例對(duì)象。接下來將開啟一個(gè) while 循環(huán),用來查找注入數(shù)據(jù)的工作。
我們知道 source 是當(dāng)前組件實(shí)例對(duì)象,在循環(huán)內(nèi)部有一個(gè) if 條件語句,如下:

if (source._provided && hasOwn(source._provided, provideKey))

該條件檢測(cè)了 source._provided 屬性是否存在,并且 source._provided 對(duì)象自身是否擁有 provideKey 鍵,如果有則說明找到了注入的數(shù)據(jù):source._provided[provideKey],并將它賦值給 result 對(duì)象的同名屬性。有的同學(xué)會(huì)問:“source 變量的初始值為當(dāng)前組件實(shí)例對(duì)象,那么如果在當(dāng)前對(duì)象下找到了通過 provide 選項(xiàng)提供的值,那豈不是自身給自身注入數(shù)據(jù)?”。大家不要忘了 inject 選項(xiàng)的初始化是在 provide 選項(xiàng)初始化之前的,也就是說即使該組件通過 provide 選項(xiàng)提供的數(shù)據(jù)中的確存在 inject 選項(xiàng)注入的數(shù)據(jù),也不會(huì)有任何影響,因?yàn)樵?inject 選項(xiàng)查找數(shù)據(jù)時(shí) provide 提供的數(shù)據(jù)還沒有被初始化,所以當(dāng)一個(gè)組件使用 provide 提供數(shù)據(jù)時(shí),該數(shù)據(jù)只有子代組件可用。
那么如果 if 判斷條件為假怎么辦?沒關(guān)系,注意 while 循環(huán)的最后一句代碼:

source = source.$parent

重新賦值 source 變量,使其引用父組件,以及類推就完成了向父代組件查找數(shù)據(jù)的需求,直到找到數(shù)據(jù)為止。
但是如果一直找到了根組件,但依然沒有找到數(shù)據(jù)怎么辦?
們知道根組件實(shí)例對(duì)象的 vm.$parent 屬性為 null,所以如上 if 條件語句的判斷條件如果成立,說明一直尋找到根組件也沒有找到要的數(shù)據(jù),此時(shí)需要查看 inject[key] 對(duì)象中是否定義了 default 選項(xiàng),如果定義了 default 選項(xiàng)則使用 default 選項(xiàng)提供的數(shù)據(jù)作為注入的數(shù)據(jù),否則在非生產(chǎn)環(huán)境下會(huì)提示開發(fā)者未找到注入的數(shù)據(jù)。

最后如果查詢到了數(shù)據(jù),resolveInject 函數(shù)會(huì)將 result 作為返回值返回,并且 result 對(duì)象的鍵就是注入數(shù)據(jù)的名字,result 對(duì)象每個(gè)鍵的值就是注入的數(shù)據(jù)。
此時(shí)我們已經(jīng)通過 resolveInject 函數(shù)取得了注入的數(shù)據(jù),并賦值給 result 常量,我們知道 result 常量的值有可能是不存在的,所以需要一個(gè) if 條件語句對(duì) result 進(jìn)行判斷,當(dāng)條件為真時(shí)說明成功取得注入的數(shù)據(jù),此時(shí)會(huì)執(zhí)行 if 語句塊內(nèi)的代碼。在 if 語句塊內(nèi)所做的事情其實(shí)很簡(jiǎn)單:

toggleObserving(false)
Object.keys(result).forEach(key => {
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, key, result[key], () => {
      warn(
        `Avoid mutating an injected value directly since the changes will be ` +
        `overwritten whenever the provided component re-renders. ` +
        `injection being mutated: "${key}"`,
        vm
      )
    })
  } else {
    defineReactive(vm, key, result[key])
  }
})
toggleObserving(true)

就是通過遍歷 result 常量并調(diào)用 defineReactive 函數(shù)在當(dāng)前組件實(shí)例對(duì)象 vm 上定義與注入名稱相同的變量,并賦予取得的值。這里有一個(gè)對(duì)環(huán)境的判斷,在非生產(chǎn)環(huán)境下調(diào)用 defineReactive 函數(shù)時(shí)會(huì)多傳遞一個(gè)參數(shù),即 customSetter,當(dāng)你嘗試設(shè)置注入的數(shù)據(jù)時(shí)會(huì)提示你不要這么做。

另外大家也注意到了在使用 defineReactive 函數(shù)為組件實(shí)例對(duì)象定義屬性之前,調(diào)用了 toggleObserving(false) 函數(shù)關(guān)閉了響應(yīng)式定義的開關(guān),之后又將開關(guān)開啟:toggleObserving(true)。前面我們已經(jīng)講到了類似的情況,這么做將會(huì)導(dǎo)致使用 defineReactive 定義屬性時(shí)不會(huì)將該屬性的值轉(zhuǎn)換為響應(yīng)式的,所以 Vue 文檔中提到了:

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

當(dāng)然啦,如果父代組件提供的數(shù)據(jù)本身就是響應(yīng)式的,即使 defineReactive 不轉(zhuǎn),那么最終這個(gè)數(shù)據(jù)也還是響應(yīng)式的。

?著作權(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ù)。

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