上一节中,我们已经知道了创建实例的大致流程,并讨论了配置的合并过程。这一节的内容就是继续讨论实例状态(state)的初始化过程。
开始
在_init中状态的初始化方法是initState,内容包括props,methods,data,computed,watcher的初始化。
export function initState (vm: Component) {
/**
* 初始化顺序
* props, methods, data, computed, watch
* @type {*[]}
* @private
*/
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
可以看到,initState中代码并不多,调用不同的init*函数来初始化实例的状态。顺序依次是props,methods,data,computed,watch。初始化顺序也决定了,data中可以使用props中的值来初始化data,computed和watch可以使用其他三种属性的值。
初始化props
在initState中调用initProps来初始化props。
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// 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)
// 这里删除了开发环境的if分支,
defineReactive(props, key, value)
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
props初始化,先获取propsData,这个值是使用组件标签由时用户传入的值,在实例上添加_props属性。接着判断是否是根节点,我们的例子使用的new Vue创建的实例所以这里为true,而toggleObserving函数是决定是否将属性转化为响应式的开关(默认为true)。最后循环所有的props属性,将属性转化为响应式的并代理到实例vm上。
校验props
调用validateProp来校验传入的props,这个函数有两个作用第一是获取默认值,第二是校验传入值类型是否正确。校验分别对type,required和validator三种进行校验,这部分我们直接忽略,有兴趣可以看看源码。那么这个函数的重点就落到获取默认值上了
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
// 是否包含Boolean类型
const booleanIndex = getTypeIndex(Boolean, prop.type)
/**
* 包含Boolean类型
* 1. 没有默认值并且父组件也没有传入,则默认为false
* 2. 父组件传入值,但是value===''或者value等于中横线形式的key
* 并且不存在String类型,或者优先级低于Boolean,则赋值为true
*/
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
// check default value
// 如果父组件传入的值为undefined,则需要转化为响应式对象
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' &&
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
assertProp(prop, key, value, vm, absent)
}
return value
}
看上去代码很多,可以分成两部分来看,第一是对获取Boolean类型默认值的特殊处理,第二是对没有传入值但是有默认值(并且是对象),要把这个对象转化为响应式对象。
那就先来看获取Boolean类型都做了什么特殊处理
- 如果没有默认值,并且没有传值则默认为false
- 父组件传入值,但是value===''或者value等于中横线形式的key,并且不存在String类型,或者优先级低于Boolean,则赋值为true
第二点稍微有点复杂,还是通过一个例子来说明,假设我们有组件并有一个Boolean类型的属性hasFlag
<MyComp hasFlag></MyComp>
<MyComp hasFlag="has-flag"></MyComp>
以上代码,hasFlag都会被认为传入了true,第一种使用方式虽然没有显示的使用hasFlag=''但在代码解析阶段所有props都会被转化为对象,值为空字符串。
最后,如果没有传入值,但是有一个默认值,就会被转化为响应式对象。如果不转,如果这个对象属性发生改变,页面将不会更新。
代理到实例上
校验完属性之后,还需要将props中的属性转化为响应式的,在父组件更新props的属性时,就可以通知渲染Watcher来更新页面。然后调用defineReactive将属性绑定到实例的_props属性上,最后调用proxy函数将props上的属性代理到实例vm上。这就是为什么我们可以通过this.xxx访问props上的属性。
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
proxy通过getter和setter将属性代理到实例vm上,注意虽然proxy支持setter,但是Vue是单向流,子组件中不允许直接修改props中的值。修改则会在控制台中发出警告。