Vue初始化都干了啥 -- 初始化props

88 阅读3分钟

上一节中,我们已经知道了创建实例的大致流程,并讨论了配置的合并过程。这一节的内容就是继续讨论实例状态(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*函数来初始化实例的状态。顺序依次是propsmethodsdatacomputedwatch。初始化顺序也决定了,data中可以使用props中的值来初始化datacomputedwatch可以使用其他三种属性的值。

初始化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,这个函数有两个作用第一是获取默认值,第二是校验传入值类型是否正确。校验分别对typerequiredvalidator三种进行校验,这部分我们直接忽略,有兴趣可以看看源码。那么这个函数的重点就落到获取默认值上了

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类型都做了什么特殊处理

  1. 如果没有默认值,并且没有传值则默认为false
  2. 父组件传入值,但是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通过gettersetter将属性代理到实例vm上,注意虽然proxy支持setter,但是Vue是单向流,子组件中不允许直接修改props中的值。修改则会在控制台中发出警告。