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)式的。