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)
}
以上代码简写为
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
// 省略...
if (process.env.NODE_ENV !== 'production') {
// 省略...
} else {
defineReactive(props, key, value)
}
// 省略...
}
toggleObserving(true)
为了搞清楚其目的,我们需要找到 defineReactive 函数,注意如下高亮的代码:
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 函数定义属性时,会调用 observe 函数对值继续进行观测。但由于之前使用了 toggleObserving(false) 函数关闭了开关,所以上面高亮代码中调用 observe 函数是一个无效调用。所以我们可以得出一个结论:在定义 props 数据时,不将 prop 值转换为响应式数据,这里要注意的是:由于 props 本身是通过 defineReactive 定义的,所以 props 本身是响应式的,但没有对值进行深度定义。为什么这样做呢?很简单,我们知道 props 是来自外界的数据,或者更具体一点的说,props 是来自父组件的数据,这个数据如果是一个对象(包括纯对象和数组),那么它本身可能已经是响应式的了,所以不再需要重复定义。另外在定义 props 数据之后,又调用 toggleObserving(true) 函数将开关开启,这么做的目的是不影响后续代码的功能,因为这个开关是全局的。
最后大家还要注意一点,如下:
if (!isRoot) {
toggleObserving(false)
}
这段代码说明,只有当不是根组件的时候才会关闭开关,这说明如果当前组件实例是根组件的话,那么定义的 props 的值也会被定义为响应式数据。
props 的校验
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 一开始并没有对 props 的类型做校验,首先如果一个 prop 的类型是布尔类型,则为其设置合理的布尔值,其次又调用了 getPropDefaultValue 函数获取 prop 的默认值,而如上这段代码才是真正用来对 props 的类型做校验的。通过如上 if 语句的条件可知,仅在非生产环境下才会对 props 做类型校验,另外还有一个条件是用来跳过 weex 环境下某种条件的判断的,我们不做讲解。总之真正的校验工作是由 assertProp 函数完成的。
methods 选项的初始化及实现
methods 选项实现要简单的多,打开 src/core/instance/state.js 文件找到 initMethods 函数,如下:
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 选项是如何实现的,就是通过 for...in 循环遍历 methods 选项对象,其中 key 就是每个方法的名字。最关键的是循环的最后一句代码:
vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
通过这句代码可知,之所以能够通过组件实例对象访问 methods 选项中定义的方法,就是因为在组件实例对象上定义了与 methods 选项中所定义的同名方法,当然了在定义到组件实例对象之前要检测该方法是否真正的有定义:methods[key] == null,如果没有则添加一个空函数到组件实例对象上。
provide 选项的初始化及实现
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')
可以发现 initInjections 函数在 initProvide 函数之前被调用,这说明对于任何一个组件来讲,总是要优先初始化 inject 选项,再初始化 provide 选项,这么做是有原因的,我们后面会提到。但是我们知道 inject 选项的数据需要从父代组件中的 provide 获取,所以我们优先来了解 provide 选项的实现,然后再查看 inject 选项的实现。
打开 src/core/instance/inject.js 文件,找到 initProvide 函数,如下:
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
如上是 initProvide 函数的全部代码,它接收组件实例对象作为参数。在 initProvide 函数内部首先定义了 provide 常量,它的值是 vm.$options.provide 选项的引用,接着是一个 if 条件语句,只有在 provide 选项存在的情况下才会执行 if 语句块内的代码,我们知道 provide 选项可以是对象,也可以是一个返回对象的函数。所以在 if 语句块内使用 typeof 操作符检测 provide 常量的类型,如果是函数则执行该函数获取数据,否则直接将 provide 本身作为数据。最后将数据复制给组件实例对象的 vm._provided 属性,后面我们可以看到当组件初始化 inject 选项时,其注入的数据就是从父代组件实例的 vm._provided 属性中获取的。
以上就是 provide 选项的初始化及实现,它本质上就是在组件实例对象上添加了 vm._provided 属性,并保存了用于子代组件的数据。
inject 选项的初始化及实现
看完了 provide 选项的初始化及实现,接下来我们研究一下 inject 选项的初始化及实现。找到 initInjections 函数,它也定义在 src/core/instance/inject.js 文件,如下是 initInjections 函数的整体结构:
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
// 省略...
}
}
找到 resolveInject 函数,它定义在 initInjections 函数的下方,如下是其函数签名:
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 选项对象的每一个键名,接下来的代码使用 for 循环,用来遍历刚刚获取到的 keys 数组,其中 key 常量就是 keys 数组中的每一个值,即 inject 选项的每一个键值,provideKey 常量保存的是每一个 inject 选项内所定义的注入对象的 from 属性的值,我们知道 from 属性的值代表着 vm._provided 数据中的每个数据的键名,所以 provideKey 常量将用来查找所注入的数据。最后定义了 source 变量,它的初始值是当前组件实例对象。接下来将开启一个 while 循环,用来查找注入数据的工作。
我们知道 source 是当前组件实例对象,在循环内部有一个 if 条件语句,如下:
if (source._provided && hasOwn(source._provided, provideKey))
该条件检测了 source._provided 属性是否存在,并且 source._provided 对象自身是否拥有 provideKey 键,如果有则说明找到了注入的数据:source._provided[provideKey],并将它赋值给 result 对象的同名属性。有的同学会问:“source 变量的初始值为当前组件实例对象,那么如果在当前对象下找到了通过 provide 选项提供的值,那岂不是自身给自身注入数据?”。大家不要忘了 inject 选项的初始化是在 provide 选项初始化之前的,也就是说即使该组件通过 provide 选项提供的数据中的确存在 inject 选项注入的数据,也不会有任何影响,因为在 inject 选项查找数据时 provide 提供的数据还没有被初始化,所以当一个组件使用 provide 提供数据时,该数据只有子代组件可用。
那么如果 if 判断条件为假怎么办?没关系,注意 while 循环的最后一句代码:
source = source.$parent
重新赋值 source 变量,使其引用父组件,以及类推就完成了向父代组件查找数据的需求,直到找到数据为止。
但是如果一直找到了根组件,但依然没有找到数据怎么办?
们知道根组件实例对象的 vm.$parent 属性为 null,所以如上 if 条件语句的判断条件如果成立,说明一直寻找到根组件也没有找到要的数据,此时需要查看 inject[key] 对象中是否定义了 default 选项,如果定义了 default 选项则使用 default 选项提供的数据作为注入的数据,否则在非生产环境下会提示开发者未找到注入的数据。
最后如果查询到了数据,resolveInject 函数会将 result 作为返回值返回,并且 result 对象的键就是注入数据的名字,result 对象每个键的值就是注入的数据。
此时我们已经通过 resolveInject 函数取得了注入的数据,并赋值给 result 常量,我们知道 result 常量的值有可能是不存在的,所以需要一个 if 条件语句对 result 进行判断,当条件为真时说明成功取得注入的数据,此时会执行 if 语句块内的代码。在 if 语句块内所做的事情其实很简单:
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 常量并调用 defineReactive 函数在当前组件实例对象 vm 上定义与注入名称相同的变量,并赋予取得的值。这里有一个对环境的判断,在非生产环境下调用 defineReactive 函数时会多传递一个参数,即 customSetter,当你尝试设置注入的数据时会提示你不要这么做。
另外大家也注意到了在使用 defineReactive 函数为组件实例对象定义属性之前,调用了 toggleObserving(false) 函数关闭了响应式定义的开关,之后又将开关开启:toggleObserving(true)。前面我们已经讲到了类似的情况,这么做将会导致使用 defineReactive 定义属性时不会将该属性的值转换为响应式的,所以 Vue 文档中提到了:
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
当然啦,如果父代组件提供的数据本身就是响应式的,即使 defineReactive 不转,那么最终这个数据也还是响应式的。