阅读 494

Vue源码学习3.9:深入Props

建议PC端观看,移动端代码高亮错乱

我们在《响应式对象》一章简单介绍了 initProps 会调用 defineReactive 来设置响应式对象。本章来深入介绍 props 相关概念~

1. 规范化

在初始化 props 之前,首先会对 props 做一次 normalize,它发生在 mergeOptions 的时候,在 src/core/util/options.js 中:

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object 
{
  // ...
  normalizeProps(child, vm)
  // ...
}

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] = { typenull }
      } 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
}
复制代码

合并配置我们在组件化章节讲过,它主要就是处理我们定义组件的对象 option,然后挂载到组件的实例 this.$options 中。

我们接下来重点看 normalizeProps 的实现,其实这个函数的主要目的就是把我们编写的 props 转成对象格式,因为实际上 props 除了对象格式,还允许写成数组格式。

  • props 是一个数组,每一个数组元素 prop 只能是一个 string,表示 propkey 并将连字符格式转成驼峰格式。
  • props 是一个对象,对于 props 中每个 propkey,我们会转驼峰格式,而它的 value,如果不是一个对象,我们就把它规范成一个对象。
  • 如果 props 既不是数组也不是对象,就抛出一个警告。

props 是一个数组的例子:

export default {
  props: ['name', 'nick-name']
}

// 经过 normalizeProps 后,会被规范成:
options.props = {
  name: { type: null },
  nickName: { type: null }
}
复制代码

props 是一个对象的例子:

export default {
  props: {
    name: String,
    nickName: {
      type: Boolean
    }
  }
}

// 经过 normalizeProps 后,会被规范成:
options.props = {
  name: { type: String },
  nickName: { type: Boolean }
}
复制代码

2. 提取父组件传递props

在之前的 《createComponent》 一章中介绍了通过 createComponent 方法创建一个占位符vnode,这其中有一个逻辑就是提取 props 并传递给子组件的过程

// src/core/vdom/create-component.js
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void 
{
  if (isUndef(Ctor)) {
    return
  }
  const baseCtor = context.$options._base

  // 这里会进行 props 的规范化,因为内部执行了mergeOptions
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  
  // 提取父组件传递的 props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  
  // ...
  
  // 将 propsData 作为参数 componentOptions 对象的属性
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefinedundefinedundefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  return vnode
}
复制代码
  • 通过 Vue.extend 创造子组件构造函数,其中会对 props 进行规范化
  • 通过 extractPropsFromVNodeData 函数提取父组件传递的 props
  • 创建 占位符vnode,并将提取后的 propsData 作为参数 componentOptions 对象的属性。

下面我们来看看如何通过 extractPropsFromVNodeData 提取父组件传递的 props

// src/core/vdom/helpers/extract-props.js
export function extractPropsFromVNodeData (
  data: VNodeData,
  Ctor: Class<Component>,
  tag?: string
): ?Object 
{
  // 我们仅在此处提取原始值。
  // 验证和默认值将在组件初始化时处理。
  
  // 此时的 propOptions 已经是规范化后的 props 对象
  const propOptions = Ctor.options.props
  if (isUndef(propOptions)) {
    return
  }
  const res = {} // 用于返回的对象
  const { attrs, props } = data // 提取data中的attrs和props
  if (isDef(attrs) || isDef(props)) {
    for (const key in propOptions) {
      const altKey = hyphenate(key) // 将驼峰格式转换成连字号格式
      if (process.env.NODE_ENV !== 'production') {
        const keyInLowerCase = key.toLowerCase()
        if (
          key !== keyInLowerCase &&
          attrs && hasOwn(attrs, keyInLowerCase)
        ) {
          tip(
            `Prop "${keyInLowerCase}" is passed to component ` +
            `${formatComponentName(tag || Ctor)}, but the declared prop name is` +
            ` "${key}". ` +
            `Note that HTML attributes are case-insensitive and camelCased ` +
            `props need to use their kebab-case equivalents when using in-DOM ` +
            `templates. You should probably use "${altKey}" instead of "${key}".`
          )
        }
      }
      // 调用checkProp优先从props里拿对应的属性,其次从attrs里拿
      // 对于attrs的话第五个参数为false,即会删除对应的attrs里的属性
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false)
    }
  }
  return res
}

function checkProp (
  res: Object,
  hash: ?Object,
  key: string,
  altKey: string,
  preserve: boolean // 保留props中的属性,删除attrs中的属性
): boolean 
{
  if (isDef(hash)) {
    if (hasOwn(hash, key)) {
      res[key] = hash[key]
      if (!preserve) {
        delete hash[key]
      }
      return true
    } else if (hasOwn(hash, altKey)) {
      res[key] = hash[altKey]
      if (!preserve) {
        delete hash[altKey]
      }
      return true
    }
  }
  return false
}
复制代码

需要说明的是这个函数的目的仅仅是为了提取原始值,验证和默认值的处理将在组件初始化时执行。

首先要知道 data 参数是什么:其实就是 createElement 的配置对象参数。它可以在我们手写 render 函数时传入,或者编译器解析模板自动生成 render 函数时传入。

函数首先获取的 propOptions 是已经规范化后的 props 对象,参考截图如下:

函数将遍历 propOptions

  • 将驼峰格式的 key 转换成连字符格式得到 altKey
  • 如果我们的 key 定义的是驼峰格式,但是 attrs 中却是全小写,说明是由于 HTML 的属性是不区分大小写导致的,因此 Vue 会抛出一个警告。
  • 调用 checkProp 优先从 props 里拿对应的属性,其次从 attrs 里拿。

函数最终得到 res 并返回。

3. 初始化

Props 的初始化发生在 initState 阶段,在 src/core/instance/state.js 中:

export function initState (vm: Component{
  // ....
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  // ...
}

function initProps (vm: Component, propsOptions: Object{
  const propsData = vm.$options.propsData || {} // 从父组件传递的 prop 数据
  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)
}
复制代码

initProps 主要做 3 件事情:校验、响应式和代理。下面分别来展开分析。

3.1 校验

校验的逻辑很简单,遍历 propsOptions,执行

validateProp(key, propsOptions, propsData, vm) 
复制代码

这里的 propsOptions 就是我们定义的 props 在规范后生成的 options.props 对象,propsData 是从父组件传递的 prop 数据。所谓校验的目的就是检查一下我们传递的数据是否满足 prop的定义规范。再来看一下 validateProp 方法,它定义在 src/core/util/props.js 中:

// src/core/util/props.js
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 类型的数据
  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 (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)
  }
  
  // prop 断言
  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 主要就做 3 件事情:

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

并最终返回 prop 的值。

3.1.1 处理 Boolean 类型的数据

Boolean 类型数据的处理逻辑:

const prop = propOptions[key] // 已规范后的prop 比如:{type: String, default: 'test'}
const absent = !hasOwn(propsData, key) // 父组件传递的对象是否有key键
let value = propsData[key] // prop的值

// 处理 Boolean 类型的数据
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
    }
  }
}
复制代码

先看下这个 getTypeIndex 方法的定义:

/**
 * Use function string name to check built-in types,
 * because a simple equality check will fail when running
 * across different vms / iframes.
 */

function getType (fn{
  const match = fn && fn.toString().match(/^\s*function (\w+)/)
  return match ? match[1] : ''
}

function isSameType (a, b{
  return getType(a) === getType(b)
}

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
}
复制代码

getTypeIndex 函数就是找到 typeexpectedTypes 匹配的索引并返回。

prop 类型定义的时候可以是某个原生构造函数,也可以是原生构造函数的数组,比如:

export default {
  props: {
    name: String,
    value: [String, Boolean]
  }
}
复制代码

如果 expectedTypes 是单个构造函数,就执行 isSameType 去判断是否是同一个类型;如果是数组,那么就遍历这个数组,找到第一个同类型的,返回它的索引。

回到 validateProp 函数看对 Boolean 的处理逻辑:

  • 先通过 const booleanIndex = getTypeIndex(Boolean, prop.type) 来判断 prop 的定义是否是 Boolean 类型的。
  • 然后得到 booleanIndex,如果 prop.type 是一个 Boolean 类型,则通过 absent && !hasOwn(prop, 'default') 来判断如果父组件没有传递这个 prop 数据并且没有设置 default 的情况,则 valuefalse
  • 接着判断 value === '' || value === hyphenate(key) 的情况,如果满足则尝试获取匹配 String 类型的索引,然后判断如果 Boolean 优先级大于 String,则 valuefalse

我们举 2 个例子来说明:

例如你定义一个组件 Student:

export default {
  name: String,
  nickName: [Boolean, String]
}
复制代码

然后在父组件中引入这个组件:

<!-- 情况1 -->
<template>
  <div>
    <student name="Kate" nickName></student>
  </div>
</template>

<!-- 情况2 -->
<template>
  <div>
    <student name="Kate" nickName="nick-name"></student>
  </div>
</template>
复制代码

第一种情况没有写属性的值,满足 value === '',第二种满足 value === hyphenate(key) 的情况,另外 nickName 这个 prop 的类型是 Boolean 或者是 String,并且满足 booleanIndex < stringIndex,所以对 nickName 这个 propvaluetrue

3.1.2 处理默认数据

来看一下默认数据处理逻辑:

// 处理默认数据
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)
}
复制代码

value 的值为 undefined 的时候,说明父组件根本就没有传这个 prop,那么我们就需要通过 getPropDefaultValue(vm, prop, key) 获取这个 prop 的默认值。我们这里只关注 getPropDefaultValue 的实现,toggleObservingobserve 的作用稍后再说。

function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
  // 没有配置默认值,直接返回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
}
复制代码

检测如果 prop 没有定义 default 属性,那么返回 undefined,通过这块逻辑我们知道除了 Boolean 类型的数据,其余没有设置 default 属性的 prop 默认值都是 undefined

接着是开发环境下对 prop 的默认值是否为对象或者数组类型的判断,如果是的话会报警告,因为对象和数组类型的 prop,他们的默认值必须要返回一个工厂函数。

接下来的判断是如果上一次组件渲染父组件传递的 prop 的值是 undefined,则直接返回 上一次的默认值 vm._props[key],这样可以避免触发不必要的 watcher 的更新。

最后就是判断 def 如果是工厂函数且 prop 的类型不是 Function 的时候,返回工厂函数的返回值,否则直接返回 def

3.1.3 prop 断言

来看下 prop 断言的逻辑:

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)
}
复制代码

在开发环境且非 weex 的某种环境下,执行 assertProp 做属性断言。

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 函数的目的是断言这个 prop 是否合法。

首先判断如果 prop 定义了 required 属性但父组件没有传递这个 prop 数据的话会报一个警告。

接着判断如果 value 为空且 prop 没有定义 required 属性则直接返回。

然后再去对 prop 的类型做校验,先是拿到 prop 中定义的类型 type,并尝试把它转成一个类型数组,然后依次遍历这个数组,执行 assertType(value, type[i]) 去获取断言的结果,直到遍历完成或者是 validtrue 的时候跳出循环。

const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/

function assertType (value: anytypeFunction): {
  valid: boolean;
  expectedType: string;
} {
  let valid
  const expectedType = getType(type)
  if (simpleCheckRE.test(expectedType)) {
    const t = typeof value
    valid = t === expectedType.toLowerCase()
    // for primitive wrapper objects
    if (!valid && t === 'object') {
      valid = value instanceof type
    }
  } else if (expectedType === 'Object') {
    valid = isPlainObject(value)
  } else if (expectedType === 'Array') {
    valid = Array.isArray(value)
  } else {
    valid = value instanceof type
  }
  return {
    valid,
    expectedType
  }
}
复制代码

assertType 的逻辑很简单,先通过 getType(type) 获取 prop 期望的类型 expectedType,然后再去根据几种不同的情况对比 prop 的值 value 是否和 expectedType 匹配,最后返回匹配的结果。

如果循环结束后 valid 仍然为 false,那么说明 prop 的值 valueprop 定义的类型都不匹配,那么就会输出一段通过 getInvalidTypeMessage(name, value, expectedTypes) 生成的警告信息,就不细说了。

最后判断当 prop 自己定义了 validator 自定义校验器,则执行 validator 校验器方法,如果校验不通过则输出警告信息。

3.2 响应式

回到 initProps 方法,当我们通过 const value = validateProp(key, propsOptions, propsData, vm)prop 做验证并且获取到 prop 的值后,接下来需要通过 defineReactiveprop 变成响应式。

defineReactive 我们之前已经介绍过,这里要注意的是,在开发环境中我们会校验 propkey 是否是 HTML 的保留属性,并且在 defineReactive 的时候会添加一个自定义 setter

!isRoot && !isUpdatingChildComponent 条件保证了在父组件可以修改 prop 而子组件直接对 prop 赋值的时候会输出警告。其中 isUpdatingChildComponent 表示是否正在调用 updateChildComponent 方法,这个方法后面还会介绍。

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)
}
复制代码

3.3 代理

在经过响应式处理后,我们会把 prop 的值添加到 vm._props 中,比如 keynameprop,它的值保存在 vm._props.name 中,但是我们在组件中可以通过 this.name 访问到这个 prop,这就是代理做的事情。

// 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)
}
复制代码

通过 proxy 函数实现了上述需求。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

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)
}
复制代码

当访问 this.name 的时候就相当于访问 this._props.name

其实对于非根实例的子组件而言,prop 的代理发生在 Vue.extend 阶段(在之前的章节也介绍过),在 src/core/global-api/extend.js 中:

Vue.extend = function (extendOptions: Object): Function {
  // ...
  const Sub = function VueComponent (options{
    this._init(options)
  }
  // ...

  // For props and computed properties, we define the proxy getters on
  // the Vue instances at extension time, on the extended prototype. This
  // avoids Object.defineProperty calls for each instance created.
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // ...
  return Sub
}

function initProps (Comp{
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}
复制代码

这么做的好处是不用为每个组件实例都做一层 proxy,是一种优化手段。、

4. Props更新

我们知道,当父组件传递给子组件的 props 值变化,子组件对应的值也会改变,同时会触发子组件的重新渲染。那么接下来我们就从源码角度来分析这两个过程。

4.1 子组件 props 更新

首先,prop 数据的值变化在父组件,我们知道在父组件的 render 过程中会访问到这个 prop 数据,所以当 prop 数据变化一定会触发父组件的重新渲染,那么重新渲染是如何更新子组件对应的 prop 的值呢?

在父组件重新渲染的最后,会执行 patch 过程,进而执行 patchVnode 函数,patchVnode 通常是一个递归过程,当它遇到组件 占位符vnode 的时候,会执行组件更新过程的 prepatch 钩子函数,在 src/core/vdom/patch.js 中:

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
{
  // ...

  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
  // ...
}
复制代码

prepatch 函数定义在 src/core/vdom/create-component.js 中:

// src/core/vdom/create-component.js
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  )
}
复制代码

内部会调用 updateChildComponent 方法来更新 props,注意第二个参数就是父组件的 propsData,它的定义在 src/core/instance/lifecycle.js 中:

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
  }

  // ...
}
复制代码

我们重点来看更新 props 的相关逻辑,这里的 propsData 是父组件传递的 props 数据,vm 是子组件的实例。vm._props 指向的就是子组件的 props 值,propKeys 就是在之前 initProps 过程中,缓存的子组件中定义的所有 propkey

主要逻辑就是遍历 propKeys,然后执行 props[key] = validateProp(key, propOptions, propsData, vm) 重新验证和计算新的 prop 数据,更新 vm._props,也就是子组件的 props

这个就是子组件 props 的更新过程。

4.2 子组件重新渲染

其实子组件的重新渲染有 2 种情况,一个是 prop 值被修改,另一个是对象类型的 prop 内部属性的变化。

先来看一下 prop 值被修改的情况,当执行 props[key] = validateProp(key, propOptions, propsData, vm) 更新子组件 prop 的时候,会触发 propsetter 过程,只要在渲染子组件的时候访问过这个 prop 值,那么根据响应式原理,就会触发子组件的重新渲染。

再来看一下当对象类型的 prop 的内部属性发生变化的时候,这个时候其实并没有触发子组件 prop 的更新。但是在子组件的渲染过程中,访问过这个对象 prop,触发 propgetter 的时候会把子组件的 render watcher 收集到依赖中,然后当我们在父组件更新这个对象 prop 的某个属性的时候,会触发 setter 过程,也就会通知子组件 render watcherupdate,进而触发子组件的重新渲染。

以上就是当父组件 props 更新,触发子组件重新渲染的 2 种情况。

5. toggleObserving

最后我们在来聊一下 toggleObserving,它的定义在 src/core/observer/index.js 中:

export let shouldObserve: boolean = true

export function toggleObserving (value: boolean{
  shouldObserve = value
}
复制代码

那么为什么在 props 的初始化和更新过程中,多次执行 toggleObserving(false) 呢,接下来我们就来分析这几种情况。

5.1 在 initProps 的过程中:

const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
  toggleObserving(false)
}
for (const key in propsOptions) {
  // ...
  const value = validateProp(key, propsOptions, propsData, vm)
  defineReactive(props, key, value)
  // ...
}
toggleObserving(true)
复制代码

对于非根实例的情况,我们会执行 toggleObserving(false),然后对于每一个 prop 值,去执行 defineReactive(props, key, value) 去把它变成响应式。

回顾一下 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,
    getfunction reactiveGetter ({
      // ...
    },
    setfunction reactiveSetter (newVal{
      // ...
    }
  })
}
复制代码

通常对于值 val 会执行 observe 函数,然后遇到 val 是对象或者数组的情况会递归执行 defineReactive 把它们的子属性都变成响应式的,但是由于 shouldObserve 的值变成了 false,这个递归过程被省略了。为什么会这样呢?

因为正如我们前面分析的,对于对象的 prop 值,子组件的 prop 值始终指向父组件的 prop 值,只要父组件的 prop 值变化,就会触发子组件的重新渲染,所以这个 observe 过程是可以省略的。

5.2 在 validateProp 的过程中:

// 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)
}
复制代码

这种是父组件没有传递 prop 值对默认值的处理逻辑,因为这个值是一个拷贝,所以我们需要 toggleObserving(true),然后执行 observe(value) 把值变成响应式。

5.3 在 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)
  // keep a copy of raw propsData
  vm.$options.propsData = propsData
}
复制代码

其实和 initProps 的逻辑一样,不需要对引用类型 props 递归做响应式处理,所以也需要 toggleObserving(false)