props处理

介绍完以上前置核心概念后,我们第一个要学习的就是Vue.js是如何处理与props相关的逻辑的。我们把与props相关的逻辑主要分成三个部分,分别是props规范化、props初始化和props更新。

props规范化

在了解规范化之前,我们先来列举一下在日常的开发过程中,我们主要有如下几种撰写组件props的方式:

  • 数组形式:props可以写成一个数组,但数组中的key元素必须为string类型。
export default {
  props: ['name', 'age']
}
  • 键值不为对象:此种方式常见于只需要定义key类型的props
export default {
  props: {
    name: String
  }
}
  • 规范格式:此种方式是Vue.js接受props最好的格式,对于一个有很高要求的组件来说,它通常会撰写很严格的props规则,这在各个开源UI框架中是最常见的。
export default {
  props: {
    name: {
      type: String,
      default: ''
    },
    age: {
      type: Number,
      default: 0,
      validator (value) {
        return value >= 0 && value <= 100
      }
    }
  }
}

props规范化所做的事情,就是把各种不是规范格式的形式,规范化为规范格式,方便Vue.js在后续的过程中处理props。那么接下来,我们就来分析Vue.js是如何对props规范化的。

props规范化的过程发生在this._init()方法中的mergeOptions合并配置中:

import { mergeOptions } from '../util/index'
export function _init (Vue) {
  const vm = this
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

其中mergeOptions()方法是定义在src/core/util/options.js文件中,它在其中有一段这样的方法调用:

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 省略代码
  normalizeProps(child, vm)
  return options
} 

我们可以发现,规范化props的代码,主要集中在normalizeProps()方法中,那么接下来我们详细分析normalizeProps()方法:

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

为了更好的理解normalizeProps()方法,我们来撰写几个案例来详细说明:

  • 数组形式:当props是数组时,会首先倒序遍历这个数组,然后使用typeof来判断数组元素的类型。如果不是string类型,则在开发环境下报错,如果是string类型,则先把key转化为驼峰形式,然后把这个key赋值到临时的res对象中,此时的键值固定为{ type: null }
// 规范化前
export default {
  props: ['age', 'nick-name']
}

// 规范化后
export default {
  props: {
    age: {
      type: null
    },
    nickName: {
      type: null
    }
  }
}
  • 对象形式:当为对象时会使用for-in遍历对象,紧接着和数组形式一样使用camelize来把key转成驼峰形式,然后使用isPlainObject()方法来判断是否为普通对象。如果不是,则转成{ type: Type }对象形式,其中Type为定义key时的Type,如果是,则直接使用这个对象。
// 规范化前
export default {
  props: {
    name: String,
    age: Number
  }
}

// 规范化后
export default {
  props: {
    name: {
      type: String
    },
    age: {
      type: Number
    }
  }
}
  • 既不是数组形式也不是对象形式:报错
// 报错:Invalid value for option "props": expected an Array or an Object,but got String
export default {
  props: 'name, age'
}

props初始化

在了解了props规范化后,我们紧接着来了解一下props初始化的过程。props初始化过程同样是发生在this._init()方法中,它在initState的时候被处理:

export function initState (vm) {
  // 省略代码
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
}

然后我们来详细看一下initProps中的代码:

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    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)
    }
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

在仔细阅读initProps()方法后,我们可以对initProps()方法进行总结,它主要做三件事情:props校验和求值props响应式props代理

props响应式

我们先来看看最简单的props响应式,这部分的过程主要使用了我们在之前介绍过的defineReactive方法:

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
    )
  }
})

唯一值得注意的地方就是:在开发环境下,props的响应式劫持了setter方法,这样做的是为了保证props为单项数据流:既我们不能在子组件中直接修改父组件传递的props值。

props代理

经过props响应式后,我们会在实例上得到this._props对象,为了方便我们更好的获取props的值,我们需要对props做一层proxy代理。关于proxy的实现,我们已经在之前的章节中介绍过了。

this._props = {
  name: '',
  age: 0
}

// 代理前
console.log(this._props.name)
proxy(vm, `_props`, key)
// 代理后
console.log(this.name)

props校验求值

最后我们来看稍微复杂一点的props校验求值,这部分的功能发生在validateProp,它的代码如下:

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虽然说的是带有校验的功能,但它并不会抛出错误进而阻止validateProp()方法返回value,而是根据校验的过程中的不同情况尽可能的提示出很清晰的提示。实质上validateProp()方法最主要的还是返回value,同时也根据不同的props写法处理不同的情况。我们可以将validateProp()方法进行总结,它主要做如下几件事情:

  • 处理Boolean类型的props
  • 处理default默认数据。
  • props断言。

那么我们接下来将分别对这几件事情进行详细的描述。

处理Boolean类型

我们先来看几个props传递Boolean的例子:

// Component A
export default {
  props: {
    fixed: Boolean
  }
}

// Component B
export default {
  props: {
    fixed: [Boolean, String]
  }
}

// Component C
export default {
  props: {
    fixed: []
  }
}

然后回到源码中处理Boolean类型getTypeIndex的地方,这个函数的代码如下:

function getTypeIndex (type, expectedTypes): number {
  if (!Array.isArray(expectedTypes)) {
    return isSameType(expectedTypes, type) ? 0 : -1
  }
  for (let i = 0, len = expectedTypes.length; i < len; i++) {
    if (isSameType(expectedTypes[i], type)) {
      return i
    }
  }
  return -1
}

这个函数的实现逻辑比较清晰:

  1. Component A组件为例,它的props不是一个数组但却是Boolean类型,因此返回索引0
  2. Component B组件为例,因为它的props是一个数组,所以要遍历这个数组,然后返回Boolean类型在数组中的索引i
  3. Component C组件为例,虽然它是一个数组,但数组中没有任何元素,因此返回索引-1

在拿到booleanIndex后,我们需要走下面这段代码逻辑:

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
    }
  }
}

代码分析:

  • if条件判断中absent代表虽然我们在子组件中定义了props,但是父组件并没有传递任何值,然后&条件又判断了子组件props有没有提供default默认值选项,如果没有,那么它的值只能为false
// 父组件未传递fixed
export default {
  name: 'ParentComponent'
  template: `<child-component />`
}

// 子组件fixed值取false
export default {
  name: 'ChildComponent',
  props: {
    fixed: Boolean
  }
}
  • else if条件判断中,我们判断了两种特殊的props传递方式:
// Parent Component A
export default {
  name: 'ParentComponentA',
  template: `<child-component fixed />`
}

// Parent Component B
export default {
  name: 'ParentComponentB',
  template: `<child-component fixed="fixed" />`
}

对于第一个种情况stringIndex-1booleanIndex0,因此value的值为true。对于第二种情况,则需要根据props的定义具体区分:

// Child Component A
export default {
  name: 'ChildComponentA'
  props: {
    fixed: [Boolean, String]
  }
}

// Child Component B
export default {
  name: 'ChildComponentB',
  props: [String, Boolean]
}
  1. 对于ChildComponentA来说,由于stringIndex值为1booleanIndex值为0booleanIndex < stringIndex因此我们可以认为Boolean具有更高的优先级,此时value的值为true
  2. 对于ChildComponentB来说,由于stringIndex值为0booleanIndex值为1stringIndex < booleanIndex因此我们可以认为String具有更高的优先级,此时value的值不处理。

处理default默认数据

处理完Boolean类型后,我们来处理默认值,既我们提到过的虽然子组件定义了props,但父组件没有传递的情况。

// 父组件未传递fixed
export default {
  name: 'ParentComponent'
  template: `<child-component />`
}

// 子组件提供了default选项
export default {
  name: 'ChildComponent',
  props: {
    fixed: {
      type: Boolean,
      default: false
    }
  }
}

对于以上案例会走如下代码的逻辑:

if (value === undefined) {
  value = getPropDefaultValue(vm, prop, key)
}

function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
  // no default, return undefined
  if (!hasOwn(prop, 'default')) {
    return undefined
  }
  const def = prop.default
  // warn against non-factory defaults for Object & Array
  if (process.env.NODE_ENV !== 'production' && isObject(def)) {
    warn(
      'Invalid default value for prop "' + key + '": ' +
      'Props with type Object/Array must use a factory function ' +
      'to return the default value.',
      vm
    )
  }
  // the raw prop value was also undefined from previous render,
  // return previous default value to avoid unnecessary watcher trigger
  if (vm && vm.$options.propsData &&
    vm.$options.propsData[key] === undefined &&
    vm._props[key] !== undefined
  ) {
    return vm._props[key]
  }
  // call factory function for non-Function types
  // a value is Function if its prototype is function even across different execution context
  return typeof def === 'function' && getType(prop.type) !== 'Function'
    ? def.call(vm)
    : def
}

代码分析:

  1. 首先判断了子组件有没有提供default默认值选项,没有则直接返回undefined
  2. 随后判断了default如果是引用类型,则提示必须把default写成一个函数,既:
default: {}
default: []

// 必须写成
default () {
  return {}
}
default () {
  return []
}
  1. 最后再根据default的类型来取值,如果是函数类型则调用这个函数,如果不是函数类型则直接使用。
  2. 其中下面一段代码在这里我们并不会说明和分析它的具体作用,而是会在props更新章节来介绍。
if (vm && vm.$options.propsData &&
  vm.$options.propsData[key] === undefined &&
  vm._props[key] !== undefined
) {
  return vm._props[key]
}

props断言

最后我们来分析一下props断言。

function assertProp (
  prop: PropOptions,
  name: string,
  value: any,
  vm: ?Component,
  absent: boolean
) {
  if (prop.required && absent) {
    warn(
      'Missing required prop: "' + name + '"',
      vm
    )
    return
  }
  if (value == null && !prop.required) {
    return
  }
  let type = prop.type
  let valid = !type || type === true
  const expectedTypes = []
  if (type) {
    if (!Array.isArray(type)) {
      type = [type]
    }
    for (let i = 0; i < type.length && !valid; i++) {
      const assertedType = assertType(value, type[i])
      expectedTypes.push(assertedType.expectedType || '')
      valid = assertedType.valid
    }
  }

  if (!valid) {
    warn(
      getInvalidTypeMessage(name, value, expectedTypes),
      vm
    )
    return
  }
  const validator = prop.validator
  if (validator) {
    if (!validator(value)) {
      warn(
        'Invalid prop: custom validator check failed for prop "' + name + '".',
        vm
      )
    }
  }
}

assertProp中我们有三种情况需要去断言:

  • required:如果子组件props提供了required选项,代表这个props必须在父组件中传递值,如果不传递则抛出错误信息Missing required prop: fixed
  • 对于定义了多个type的类型数组,则我们会遍历这个类型数组,只要当前props的类型和类型数组中某一个元素匹配则终止遍历。,否则抛出错误提示信息。
// Parent Component
export default {
  name: 'ParentComponent',
  template: `<child-component :age="true" />`
}
// Chil Component
export default {
  name: 'ChilComponent',
  props: {
    age: [Number, String]
  }
}

// 报错:Invalid prop: type check failed for prop age,Expected Number, String,got with value true
  • 用户自己提供的validator校验器我们也需要进行断言:
// Parent Component
export default {
  name: 'ParentComponent',
  template: `<child-component :age="101" />`
}
// Chil Component
export default {
  name: 'ChilComponent',
  props: {
    age: {
      type: Number,
      validator (value) {
        return value >=0 && value <=100
      }
    }
  }
}

// 报错:Invalid prop: custom validator check failed for prop age

props更新

我们都知道子组件的props值来源于父组件,当父组件值更新时,子组件的值也会发生改变,同时触发子组件的重新渲染。我们先跳过父组件的具体编译逻辑,直接看父组件的值更新,改变子组件props值的步骤:

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // 省略代码
  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }
}

代码分析:

  1. 以上vm实例为子组件,propsData为父组件中传递的props的值,而_propKeys是之前props初始化过程中缓存起来的所有的props的key。
  2. 在父组件值更新后,会通过遍历propsKey来重新对子组件props进行校验求值,最后赋值。

以上代码就是子组件props更新的过程,在props更新后会进行子组件的重新渲染,这个重新渲染的过程分两种情况:

  • 普通props值被修改:当props值被修改后,其中有段代码props[key] = validateProp(key, propOptions, propsData, vm)根据响应式原理,会触发属性的setter,进而子组件可以重新渲染。
  • 对象props内部属性变化:当这种情况发生时,并没有触发子组件prop的更新,但是在子组件渲染的时候读取到了props,因此会收集到这个propsrender watcher,当对象props内部属性变化的时候,根据响应式原理依然会触发setter,进而子组件可以重新进行渲染。

toggleObserving作用

toggleObserving是定义在src/core/observer/index.js文件中的一个函数,其代码很简单:

export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
  shouldObserve = value
}

它的作用就是修改当前模块的shouldObserve变量,用来控制在observe的过程中是否需要把当前值变成一个observer对象。

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

接下来我们来分析,在处理props的过程中,什么时候toggleObserving(true),什么时候toggleObserving(false)以及为什么需要这样处理?

function initProps (vm: Component, propsOptions: Object) {
  if (!isRoot) {
    toggleObserving(false)
  }
  // 省略defineReactive的过程
  toggleObserving(true)
}

props初始化的时候:
我们可以看到在最开始判断了当为非根实例(子组件)的时候,进行了toggleObserving(false)的操作,这样做的目的是因为:当非根实例的时候,组件的props来自于父组件。当props为对象或者数组时,根据响应式原理,我们会递归遍历子属性然后进行observe(val),而正是因为props来源于父组件,这个过程其实已经在父组件执行过了,如果不做任何限制,那么会在子组件中又重复一次这样的过程,因此这里需要toggleObserving(false),用来避免递归props子属性的情况,这属于响应式优化的一种手段。在代码最后,又调用了toggleObserving(true),把shouldObserve的值还原。

props校验的时候:
我们先来看props提供了default默认值,且默认值返回了对象或者数组。

export default {
  props: {
    point: {
      type: Object,
      default () {
        return {
          x: 0,
          y: 0
        }
      }
    },
    list: {
      type: Array,
      default () {
        return []
      }
    }
  }
}

对于以上pointlist取默认值的情况,这个时候的props值与父组件没有关系,那么这个时候我们需要toggleObserving(true),在observe后再把shouldObserve变量设置为原来的值。

export function validateProp () {
  // 省略代码
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    const prevShouldObserve = shouldObserve 
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
}

props更新的时候:
当父组件更新的时候,会调用updateChildComponent()方法,用来更新子组件的props值,这个时候其实和props初始化的逻辑一样,我们同样不需要对指向父组件的对象或数组props进行递归子属性observe的过程,因此这里需要执行toggleObserving(false)

export function updateChildComponent () {
  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    vm.$options.propsData = propsData
  }
}

整体流程图

在分析完以上所有与props相关的逻辑后,我们可以总结如下流程图。

最后更新时间:
贡献者: wangtunan