Vue Props 应用与源码分析

Vue组件的属性提供了父组件向子组件传递自身数据状态的途径, 这里我们简单总结一下属性的使用方式和特性,以及背后的源码实现。

应用

组件使用Props来接收外部所传递来的数据,通常可以分为三类:

1. 自定义属性:开发者在组件props字段中声明和定义的属性。

在官方的示例中,列举了子组件具体可以声明的属性类型和校验方式:

Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组(引用类型的)默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

type字段指定了属性所属类型的构造函数,可以包括以下几种原生类型:

String, Number, Boolean, Array, Object, Date, Function, Symbol

除了原生构造函数之外, 我们还可以自定义构造函数,来验证作为属性值的实例对象是否是通过该构造函数创建的。

2. 原生属性:也就是未在组件内部声明的属性,默认会自动挂载的组件根元素上(inheritAttrs为false可以禁止自动挂载).

在一个组件内部显示声明的属性不仅定义了组件从外部接收数据的类型与格式,同时也确定了所接收数据在组件中的用途和处理方式. 而对于那些无法预见到的业务场景和传入的数据信息, 组件选择默认挂载到根元素上。

TIP

通过组件的inheritAttrs字段可以禁止这一特性, 同时结合组件实例的attrs属性,可以将原生属性指定挂载到固定的子元素上。

3. 特殊属性:例如class,style, ref, key等等, 挂载在组件根元素上.

特殊属性其实也是非自定义属性的一种,这里单拿出来分类的原因是它与原生属性不太一样,其特殊性在于: inheritAttrs字段配置不会对特殊属性生效;另外,对于classstyle等属性,组件外部传入的属性值与组件的内部定义的属性值会进行智能地合并;

Note

由于HTML对于大小写不敏感,对于采用驼峰方式命名的自定义组件名称或属性名,在DOM中使用的时候需要采用kebab-case方式使用。

单向数据流

在Vue的组件树结构中,父组件到子组件之间的数据流向是单向的:父组件可以通过Prop将数据传递给子组件,而不允许子组件内部去更改Prop「One-Way Data Flow」

之所以做出这样限制的原因在于,一个父组件中可能包含了多个子组件,这些子组件都通过Prop依赖了父组件中的数据。

如果某一个子组件内部任意修改了Prop数据(不经过父组件的允许),也就是修改了父组件中的数据和状态,很可能对其他子组件造成影响。 因此,更好的方式是通过事件将修改数据状态的请求抛出给父组件,由父组件修改自身的数据状态。

尽管修改引用类型的Prop时候Vue不会报错,但仍会引起父组件数据状态变化。

实现

介绍了属性的使用姿势和特性,我们来具体看看Vue是如何实现属性的校验、单向数据流等机制的。

规范化

像上面属性的应用中所介绍的那样,Vue组件支持了很多种定义属性的方式,包括默认值,校验函数等等。 为了便于后续处理和操作,需要对各种语法使用进行兼容操作。

这部分在mergeOptions操作的时候进行了处理。

初始化

组件属性的初始化发生在组件生命周期beforeCreated之后,在initProps中执行:

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // 缓存属性的key到vm的_propKeys属性,方便后面的遍历操作
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {// 如果当前不是Vue的根实例,关闭掉数据依赖收集
    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 (vm.$parent && !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)
}

组件属性的初始化过程中,会校验了属性的合法性,除了用户在声明属性时的类型和校验函数,还会根据平台判断属性是否为保留的关键字。

我们知道,子组件通过属性的方式依赖了父组件的数据状态,同时采用了单向数据流的设计,不允许子组件修改父组件的数据状态。 为此,defineReactive在对于属性的响应式处理的时候,做了两个操作:

  • setter方法中,当子组件试图修改属性值的时候报警告提示:
function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val
    /* eslint-disable no-self-compare */
  ...
    /* eslint-enable no-self-compare */
  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
  }
  ...
}
  • 只有当Vue实例为根组件实例的时候,才递归遍历属性对象,声明为响应式数据:
  // initProps
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {// 如果当前不是Vue的根实例,关闭掉数据依赖收集
    toggleObserving(false)
  }

  ...
  
  // defineReactive
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    ...
}

关于响应式数据的声明,更多具体内容见「Vue 响应式系统的原理」