{T}

前文提到过 Vue.js 的核心思想之一是组件化,页面可以由一个个组件构建而成,组件是一种抽象的概念,它是对页面的部分布局和逻辑的封装。

为了让组件支持各种丰富的功能,Vue.js 设计了 Props 特性,它允许组件的使用者在外部传递 Props,然后组件内部就可以根据这些 Props 去实现各种各样的功能。

为了演示 Props 的作用,定义一个 BlogPost 组件,接收 title 和 author 两个 Prop:

html
<div class="blog-post">
  <h1>{{title}}</h1>
  <p>author: {{author}}</p>
</div>
<script>
  export default {
    props: {
      title: String,
      author: String
    }
  }
</script>

然后我们在父组件使用这个 BlogPost 组件的时候,可以给它传递一些 Props 数据:

html
<blog-post title="Vue3 publish" author="yyx"></blog-post>

从最终结果来看,BlogPost 组件会渲染传递的 title 和 author 数据。

我们平时写组件会经常和 Props 打交道,但 Vue.js 内部是如何初始化以及更新 Props 的?Vue.js 3.0 在 props 的 API 设计上与 Vue.js 2.x 保持一致,底层实现层面则有所不同。

Props 的初始化

先了解 Props 的初始化过程。前面介绍 Setup 组件初始化时提到,执行 setupComponent 函数会初始化 Props:

js
function setupComponent (instance, isSSR = false) {
  const { props, children, shapeFlag } = instance.vnode
  // 判断是否是一个有状态的组件
  const isStateful = shapeFlag & 4
  // 初始化 props
  initProps(instance, props, isStateful, isSSR)
  // 初始化插槽
  initSlots(instance, children)
  // 设置有状态的组件实例
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  return setupResult
}

Props 初始化通过 initProps 完成,看它的实现:

js
function initProps(instance, rawProps, isStateful, isSSR = false) {
  const props = {}
  const attrs = {}
  def(attrs, InternalObjectKey, 1)
  // 设置 props 的值
  setFullProps(instance, rawProps, props, attrs)
  // 验证 props 合法
  if ((process.env.NODE_ENV !== 'production')) {
    validateProps(props, instance.type)
  }
  if (isStateful) {
    // 有状态组件,响应式处理
    instance.props = isSSR ? props : shallowReactive(props)
  }
  else {
    // 函数式组件处理
    if (!instance.type.props) {
      instance.props = attrs
    }
    else {
      instance.props = props
    }
  }
  // 普通属性赋值
  instance.attrs = attrs
}

这里,初始化 Props 主要做了以下几件事情:设置 props 的值验证 props 是否合法把 props 变成响应式以及添加到实例 instance.props 上

注意,这里我们只分析有状态组件的 Props 初始化过程,所以就默认 isStateful 的值是 true。所谓有状态组件,就是你平时通过对象的方式定义的组件。

设置 Props 的流程如下。

设置 Props

setFullProps 的实现:

js
function setFullProps(instance, rawProps, props, attrs) {
  // 标准化 props 的配置
  const [options, needCastKeys] = normalizePropsOptions(instance.type)
  if (rawProps) {
    for (const key in rawProps) {
      const value = rawProps[key]
      // 一些保留的 prop 比如 ref、key 是不会传递的
      if (isReservedProp(key)) {
        continue
      }
      // 连字符形式的 props 也转成驼峰形式
      let camelKey
      if (options && hasOwn(options, (camelKey = camelize(key)))) {
        props[camelKey] = value
      }
      else if (!isEmitListener(instance.type, key)) {
        // 非事件派发相关的,且不在 props 中定义的普通属性用 attrs 保留
        attrs[key] = value
      }
    }
  }
  if (needCastKeys) {
    // 需要做转换的 props
    const rawCurrentProps = toRaw(props)
    for (let i = 0; i < needCastKeys.length; i++) {
      const key = needCastKeys[i]
      props[key] = resolvePropValue(options, rawCurrentProps, key, rawCurrentProps[key])
    }
  }
}

先说明函数几个参数的含义:instance 表示组件实例;rawProps 表示原始的 props 值,也就是创建 vnode 过程中传入的 props 数据;props 用于存储解析后的 props 数据;attrs 用于存储解析后的普通属性数据。

设置 Props 的过程也分成几个步骤:标准化 props 的配置,遍历 props 数据求值,以及对需要转换的 props 求值。

标准化 props 配置的过程,看 normalizePropsOptions 的实现:

js
function normalizePropsOptions(comp) {
  // comp.__props 用于缓存标准化的结果,有缓存,则直接返回
  if (comp.__props) {
    return comp.__props
  }
  const raw = comp.props
  const normalized = {}
  const needCastKeys = []
  // 处理 mixins 和 extends 这些 props
  let hasExtends = false
  if (!shared.isFunction(comp)) {
    const extendProps = (raw) => {
      const [props, keys] = normalizePropsOptions(raw)
      shared.extend(normalized, props)
      if (keys)
        needCastKeys.push(...keys)
    }
    if (comp.extends) {
      hasExtends = true
      extendProps(comp.extends)
    }
    if (comp.mixins) {
      hasExtends = true
      comp.mixins.forEach(extendProps)
    }
  }
  if (!raw && !hasExtends) {
    return (comp.__props = shared.EMPTY_ARR)
  }
  // 数组形式的 props 定义
  if (shared.isArray(raw)) {
    for (let i = 0; i < raw.length; i++) {
      if (!shared.isString(raw[i])) {
        warn(`props must be strings when using array syntax.`, raw[i])
      }
      const normalizedKey = shared.camelize(raw[i])
      if (validatePropName(normalizedKey)) {
        normalized[normalizedKey] = shared.EMPTY_OBJ
      }
    }
  }
  else if (raw) {
    if (!shared.isObject(raw)) {
      warn(`invalid props options`, raw)
    }
    for (const key in raw) {
      const normalizedKey = shared.camelize(key)
      if (validatePropName(normalizedKey)) {
        const opt = raw[key]
        // 标准化 prop 的定义格式
        const prop = (normalized[normalizedKey] =
          shared.isArray(opt) || shared.isFunction(opt) ? { type: opt } : opt)
        if (prop) {
          const booleanIndex = getTypeIndex(Boolean, prop.type)
          const stringIndex = getTypeIndex(String, prop.type)
          prop[0 /* shouldCast */] = booleanIndex > -1
          prop[1 /* shouldCastTrue */] =
            stringIndex < 0 || booleanIndex < stringIndex
          // 布尔类型和有默认值的 prop 都需要转换
          if (booleanIndex > -1 || shared.hasOwn(prop, 'default')) {
            needCastKeys.push(normalizedKey)
          }
        }
      }
    }
  }
  const normalizedEntry = [normalized, needCastKeys]
  comp.__props = normalizedEntry
  return normalizedEntry
}

normalizePropsOptions 主要目的是标准化 props 的配置,这里需要注意,你要区分 props 的配置和 props 的数据。所谓 props 的配置,就是你在定义组件时编写的 props 配置,它用来描述一个组件的 props 是什么样的;而 props 的数据,是父组件在调用子组件的时候,给子组件传递的数据。

所以这个函数首先会处理 mixins 和 extends 这两个特殊的属性,因为它们的作用都是扩展组件的定义,所以需要对它们定义中的 props 递归执行 normalizePropsOptions。

接着,函数会处理数组形式的 props 定义,例如:

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

如果 props 被定义成数组形式,那么数组的每个元素必须是一个字符串,然后把字符串都变成驼峰形式作为 key,并为normalized 的 key 对应的每一个值创建一个空对象。针对上述示例,最终标准化的 props 的定义是这样的:

java
export default {
  props: {
    name: {},
    nickName: {}
  }
}

如果 props 定义是一个对象形式,接着就是标准化它的每一个 prop 的定义,把数组或者函数形式的 prop 标准化成对象形式,例如:

java
export default {
  title: String,
  author: [String, Boolean]
}

注意,上述代码中的 String 和 Boolean 都是内置的构造器函数。经过标准化的 props 的定义:

java
export default {
  props: {
    title: {
      type: String
    },
    author: {
      type: [String, Boolean]
    }
  }
}

接下来,就是判断一些 prop 是否需要转换,其中,含有布尔类型的 prop 和有默认值的 prop 需要转换,这些 prop 的 key 保存在 needCastKeys 中。注意,这里会给 prop 添加两个特殊的 key,prop[0] 和 prop[1]赋值,它们的作用后续我们会说。

最后,返回标准化结果 normalizedEntry,它包含标准化后的 props 定义 normalized,以及需要转换的 props key needCastKeys,并且用 comp.__props 缓存这个标准化结果,如果对同一个组件重复执行 normalizePropsOptions,直接返回这个标准化结果即可。

标准化 props 配置的目的无非就是支持用户各种的 props 配置写法,标准化统一的对象格式为了后续统一处理。

回到 setFullProps,分析遍历 props 数据求值的流程。

js
function setFullProps(instance, rawProps, props, attrs) {
  // 标准化 props 的配置

  if (rawProps) {
    for (const key in rawProps) {
      const value = rawProps[key]
      // 一些保留的 prop 比如 ref、key 是不会传递的
      if (isReservedProp(key)) {
        continue
      }
      // 连字符形式的 props 也转成驼峰形式
      let camelKey
      if (options && hasOwn(options, (camelKey = camelize(key)))) {
        props[camelKey] = value
      }
      else if (!isEmitListener(instance.type, key)) {
        // 非事件派发相关的,且不在 props 中定义的普通属性用 attrs 保留
        attrs[key] = value
      }
    }
  }

  // 转换需要转换的 props
}

该过程主要就是遍历 rawProps,拿到每一个 key。由于我们在标准化 props 配置过程中已经把 props 定义的 key 转成了驼峰形式,所以也需要把 rawProps 的 key 转成驼峰形式,然后对比看 prop 是否在配置中定义。

如果 rawProps 中的 prop 在配置中定义了,那么把它的值赋值到 props 对象中,如果不是,那么判断这个 key 是否为非事件派发相关,如果是那么则把它的值赋值到 attrs 对象中。另外,在遍历的过程中,遇到 key、ref 这种 key,则直接跳过。

setFullProps 的最后一个流程:对需要转换的 props 求值。

js
function setFullProps(instance, rawProps, props, attrs) {
  // 标准化 props 的配置

  // 遍历 props 数据求值

  if (needCastKeys) {
    // 需要做转换的 props
    const rawCurrentProps = toRaw(props)
    for (let i = 0; i < needCastKeys.length; i++) {
      const key = needCastKeys[i]
      props[key] = resolvePropValue(options, rawCurrentProps, key, rawCurrentProps[key])
    }
  }
}

在 normalizePropsOptions 中已经拿到需要转换的 props 的 key,接下来遍历 needCastKeys,依次执行 resolvePropValue 求值。看它的实现:

js
function resolvePropValue(options, props, key, value) {
  const opt = options[key]
  if (opt != null) {
    const hasDefault = hasOwn(opt, 'default')
    // 默认值处理
    if (hasDefault && value === undefined) {
      const defaultValue = opt.default
      value =
        opt.type !== Function && isFunction(defaultValue)
          ? defaultValue()
          : defaultValue
    }
    // 布尔类型转换
    if (opt[0 /* shouldCast */]) {
      if (!hasOwn(props, key) && !hasDefault) {
        value = false
      }
      else if (opt[1 /* shouldCastTrue */] &&
        (value === '' || value === hyphenate(key))) {
        value = true
      }
    }
  }
  return value
}

resolvePropValue 主要就是针对两种情况的转换,第一种是默认值的情况,即我们在 prop 配置中定义了默认值,并且父组件没有传递数据的情况,这里 prop 对应的值就取默认值。

第二种是布尔类型的值,前面我们在 normalizePropsOptions 的时候已经给 prop 的定义添加了两个特殊的 key,所以 opt[0] 为 true 表示这是一个含有 Boolean 类型的 prop,然后判断是否有传对应的值,如果不是且没有默认值的话,就直接转成 false,举个例子:

java
export default {
  props: {
    author: Boolean
  }
}

如果父组件调用子组件的时候没有给 author 这个 prop 传值,那么它转换后的值就是 false。

接着看 opt[1] 为 true,并且 props 传值是空字符串或者是 key 字符串的情况,命中这个逻辑表示这是一个含有 Boolean 和 String 类型的 prop,且 Boolean 在 String 前面,例如:

java
export default {
  props: {
    author: [Boolean, String]
  }
}

这种时候如果传递的 prop 值是空字符串,或者是 author 字符串,则 prop 的值会被转换成 true。

至此 props 的转换求值结束,setFullProps 的整个逻辑也随之结束。回顾它的完整流程,其主要目的就是对 props 求值然后把求得的值赋值给 props 对象和 attrs 对象

验证 Props

回到 initProps,分析第二个流程:验证 props 是否合法。

js
function initProps(instance, rawProps, isStateful, isSSR = false) {
  const props = {}
  // 设置 props 的值

  // 验证 props 合法
  if ((process.env.NODE_ENV !== 'production')) {
    validateProps(props, instance.type)
  }
}

验证过程在非生产环境下执行,看 validateProps 的实现:

js
function validateProps(props, comp) {
  const rawValues = toRaw(props)
  const options = normalizePropsOptions(comp)[0]
  for (const key in options) {
    let opt = options[key]
    if (opt == null)
      continue
    validateProp(key, rawValues[key], opt, !hasOwn(rawValues, key))
  }
}
function validateProp(name, value, prop, isAbsent) {
  const { type, required, validator } = prop
  // 检测 required
  if (required && isAbsent) {
    warn('Missing required prop: "' + name + '"')
    return
  }
  // 虽然没有值但也没有配置 required,直接返回
  if (value == null && !prop.required) {
    return
  }
  // 类型检测
  if (type != null && type !== true) {
    let isValid = false
    const types = isArray(type) ? type : [type]
    const expectedTypes = []
    // 只要指定的类型之一匹配,值就有效
    for (let i = 0; i < types.length && !isValid; i++) {
      const { valid, expectedType } = assertType(value, types[i])
      expectedTypes.push(expectedType || '')
      isValid = valid
    }
    if (!isValid) {
      warn(getInvalidTypeMessage(name, value, expectedTypes))
      return
    }
  }
  // 自定义校验器
  if (validator && !validator(value)) {
    warn('Invalid prop: custom validator check failed for prop "' + name + '".')
  }
}

顾名思义,validateProps 就是用来检测前面求得的 props 值是否合法,它就是对标准化后的 Props 配置对象进行遍历,拿到每一个配置 opt,然后执行 validateProp 验证。

对于单个 Prop 的配置,我们除了配置它的类型 type,还可以配置 required 表明它的必要性,以及 validator 自定义校验器,举个例子:

java
export default {
  props: {
    value: {
      type: Number,
      required: true,
      validator(val) {
        return val >= 0
      }
    }
  }
}

因此 validateProp 首先验证 required 的情况,一旦 prop 配置了 required 为 true,那么必须给它传值,否则会报警告。

接着是验证 prop 值的类型,由于 prop 定义的 type 可以是多个类型的数组,那么只要 prop 的值匹配其中一种类型,就是合法的,否则会报警告。

最后是验证如果配了自定义校验器 validator,那么 prop 的值必须满足自定义校验器的规则,否则会报警告。

响应式处理

回到 initProps,看最后一个流程:把 props 变成响应式,添加到实例 instance.props 上。

js
function initProps(instance, rawProps, isStateful, isSSR = false) {
  // 设置 props 的值
  // 验证 props 合法
  if (isStateful) {
    // 有状态组件,响应式处理
    instance.props = isSSR ? props : shallowReactive(props)
  }
  else {
    // 函数式组件处理
    if (!instance.type.props) {
      instance.props = attrs
    }
    else {
      instance.props = props
    }
  }
  // 普通属性赋值
  instance.attrs = attrs
}

在前两个流程,我们通过 setFullProps 求值赋值给 props 变量,并对 props 做了检测,接下来,就是把 props 变成响应式,并且赋值到组件的实例上。

Props 的更新

所谓 Props 的更新主要是指 Props 数据的更新,它最直接的反应是会触发组件的重新渲染,我们可以通过一个简单的示例分析这个过程。例如我们有这样一个子组件 HelloWorld,它是这样定义的:

html
<template>
  <div>
    <p>{{ msg }}</p>
  </div>
</template>
<script>
  export default {
    props: {
      msg: String
    }
  }
</script>

这里,HelloWorld 组件接受一个 msg prop,然后在模板中渲染这个 msg。

然后我们在 App 父组件中引入这个子组件,它的定义如下:

html
<template>
  <hello-world :msg="msg"></hello-world>
  <button @click="toggleMsg">Toggle Msg</button>
</template>
<script>
  import HelloWorld from './components/HelloWorld'
  export default {
    components: { HelloWorld },
    data() {
      return {
        msg: 'Hello world'
      }
    },
    methods: {
      toggleMsg() {
        this.msg = this.msg === 'Hello world' ? 'Hello Vue' : 'Hello world'
      }
    }
  }
</script>

给 HelloWorld 子组件传递的 prop 值是 App 组件中定义的 msg 变量,初始值为 Hello world,会在子组件模板中显示。点击按钮修改 msg 的值会触发父组件重新渲染,因为模板引用了 msg 变量。此时 HelloWorld 子组件显示的字符串变为 Hello Vue,子组件是如何被触发重新渲染的?

在组件更新的章节我们说过,组件的重新渲染会触发 patch 过程,然后遍历子节点递归 patch,那么遇到组件节点,会执行 updateComponent 方法:

js
const updateComponent = (n1, n2, parentComponent, optimized) => {
  const instance = (n2.component = n1.component)
  // 根据新旧子组件 vnode 判断是否需要更新子组件
  if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
    // 新的子组件 vnode 赋值给 instance.next
    instance.next = n2
    // 子组件也可能因为数据变化被添加到更新队列里了,移除它们防止对一个子组件重复更新
    invalidateJob(instance.update)
    // 执行子组件的副作用渲染函数
    instance.update()
  }
  else {
    // 不需要更新,只复制属性
    n2.component = n1.component
    n2.el = n1.el
  }
}

在这个过程中,会执行 shouldUpdateComponent 方法判断是否需要更新子组件,内部会对比 props,由于我们的 prop 数据 msg 由 Hello world 变成了 Hello Vue,值不一样所以 shouldUpdateComponent 会返回 true,这样就把新的子组件 vnode 赋值给 instance.next,然后执行 instance.update 触发子组件的重新渲染。

这就是触发子组件重新渲染的原因。但子组件重新渲染后,其 instance.props 的数据也需要更新,否则渲染的仍是之前的数据,下面看 instance.props 是如何更新的。

执行 instance.update 函数,实际上是执行 componentEffect 组件副作用渲染函数:

js
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
  // 创建响应式的副作用渲染函数
  instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
      // 渲染组件
    }
    else {
      // 更新组件
      let { next, vnode } = instance
      // next 表示新的组件 vnode
      if (next) {
        // 更新组件 vnode 节点信息
        updateComponentPreRender(instance, next, optimized)
      }
      else {
        next = vnode
      }
      // 渲染新的子树 vnode
      const nextTree = renderComponentRoot(instance)
      // 缓存旧的子树 vnode
      const prevTree = instance.subTree
      // 更新子树 vnode
      instance.subTree = nextTree
      // 组件更新核心逻辑,根据新旧子树 vnode 做 patch
      patch(prevTree, nextTree,
        // 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点
        hostParentNode(prevTree.el),
        // 参考节点在 fragment 的情况可能改变,所以直接找旧树 DOM 元素的下一个节点
        getNextHostNode(prevTree),
        instance,
        parentSuspense,
        isSVG)
      // 缓存更新后的 DOM 节点
      next.el = nextTree.el
    }
  }, prodEffectOptions)
}

更新组件时判断是否有 instance.next(代表新的组件 vnode)。根据前面逻辑 next 不为空,于是执行 updateComponentPreRender 更新组件 vnode 节点信息,看它的实现:

js
const updateComponentPreRender = (instance, nextVNode, optimized) => {
  nextVNode.component = instance
  const prevProps = instance.vnode.props
  instance.vnode = nextVNode
  instance.next = null
  updateProps(instance, nextVNode.props, prevProps, optimized)
  updateSlots(instance, nextVNode.children)
}

其中会执行 updateProps 更新 props 数据,看它的实现:

js
function updateProps(instance, rawProps, rawPrevProps, optimized) {
  const { props, attrs, vnode: { patchFlag } } = instance
  const rawCurrentProps = toRaw(props)
  const [options] = normalizePropsOptions(instance.type)
  if ((optimized || patchFlag > 0) && !(patchFlag & 16 /* FULL_PROPS */)) {
    if (patchFlag & 8 /* PROPS */) {
      // 只更新动态 props 节点
      const propsToUpdate = instance.vnode.dynamicProps
      for (let i = 0; i < propsToUpdate.length; i++) {
        const key = propsToUpdate[i]
        const value = rawProps[key]
        if (options) {
          if (hasOwn(attrs, key)) {
            attrs[key] = value
          }
          else {
            const camelizedKey = camelize(key)
            props[camelizedKey] = resolvePropValue(options, rawCurrentProps, camelizedKey, value)
          }
        }
        else {
          attrs[key] = value
        }
      }
    }
  }
  else {
    // 全量 props 更新
    setFullProps(instance, rawProps, props, attrs)
    // 因为新的 props 是动态的,把那些不在新的 props 中但存在于旧的 props 中的值设置为 undefined
    let kebabKey
    for (const key in rawCurrentProps) {
      if (!rawProps ||
        (!hasOwn(rawProps, key) &&
          ((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))) {
        if (options) {
          if (rawPrevProps &&
            (rawPrevProps[key] !== undefined ||
              rawPrevProps[kebabKey] !== undefined)) {
            props[key] = resolvePropValue(options, rawProps || EMPTY_OBJ, key, undefined)
          }
        }
        else {
          delete props[key]
        }
      }
    }
  }
  if ((process.env.NODE_ENV !== 'production') && rawProps) {
    validateProps(props, instance.type)
  }
}

updateProps 的主要目标就是把父组件渲染时求得的 props 新值更新到子组件实例的 instance.props 中。

编译阶段除了捕获动态 vnode,也捕获了动态 props,因此可只比对动态 props 数据更新;不满足优化条件时则通过 setFullProps 全量比对,并把那些不在新 props 中但存在于旧 props 中的值设置为 undefined。

至此已清楚子组件实例 props 值是如何更新的。一个需要思考的问题是:为什么 instance.props 需要变成响应式?这是出于需求——希望在子组件中可以监听 props 值的变化做一些事情,例如:

js
import { ref, h, defineComponent, watchEffect } from 'vue'
const count = ref(0)
let dummy
const Parent = {
  render: () => h(Child, { count: count.value })
}
const Child = defineComponent({
  props: { count: Number },
  setup(props) {
    watchEffect(() => {
      dummy = props.count
    })
    return () => h('div', props.count)
  }
})
count.value++

这里,我们定义了父组件 Parent 和子组件 Child,子组件 Child 中定义了 prop count,除了在渲染模板中引用了 count,我们在 setup 函数中通过了 watchEffect 注册了一个回调函数,内部依赖了 props.count,当修改 count.value 的时候,我们希望这个回调函数也能执行,所以这个 prop 的值需要是响应式的,由于 setup 函数的第一个参数是props 变量,其实就是组件实例 instance.props,所以也就是要求 instance.props 是响应式的。

再看为什么用 shallowReactive:shallow 的字面意思是浅的,从实现上来说,就是不会递归执行 reactive,只劫持最外一层对象。

shallowReactive 和普通的 reactive 函数的主要区别是处理器函数不同,我们来回顾 getter 的处理器函数:

js
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    if (key === "__v_isReactive" /* IS_REACTIVE */) {
      return !isReadonly;
    }
    else if (key === "__v_isReadonly" /* IS_READONLY */) {
      return isReadonly;
    }
    else if (key === "__v_raw" /* RAW */ &&
      receiver ===
      (isReadonly
        ? target["__v_readonly" /* READONLY */]
        : target["__v_reactive" /* REACTIVE */])) {
      return target;
    }
    const targetIsArray = isArray(target);
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver);
    }
    const res = Reflect.get(target, key, receiver);
    if (isSymbol(key)
      ? builtInSymbols.has(key)
      : key === `__proto__` || key === `__v_isRef`) {
      return res;
    }
    if (!isReadonly) {
      track(target, "get" /* GET */, key);
    }
    if (shallow) {
      return res;
    }
    if (isRef(res)) {
      return targetIsArray ? res : res.value;
    }
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res);
    }
    return res;
  };
}

shallowReactive 创建的 getter 函数,shallow 变量为 true,那么就不会执行后续的递归 reactive 逻辑。也就是说,shallowReactive 只把对象 target 的最外一层属性的访问和修改处理成响应式。

之所以可以这么做,是因为 props 在更新的过程中,只会修改最外层属性,所以用 shallowReactive 就足够了。

总结

html
<template>
  <div>
    <p>{{ msg }}</p>
    <p>{{ info.name }}</p>
    <p>{{ info.age }}</p>
  </div>
</template>
<script>
  export default {
    props: {
      msg: String,
      info: Object
    }
  }
</script>

下面在子组件增加 info prop 并渲染其子属性,再修改父组件:

html
<template>
  <hello-world :msg="msg" :info="info"></hello-world>
  <button @click="addAge">Add age</button>
  <button @click="toggleMsg">Toggle Msg</button>
</template>
<script>
  import HelloWorld from './components/HelloWorld'
  export default {
    components: { HelloWorld },
    data() {
      return {
        info: {
          name: 'Tom',
          age: 18
        },
        msg: 'Hello world'
      }
    },
    methods: {
      addAge() {
        this.info.age++
      },
      toggleMsg() {
        this.msg = this.msg === 'Hello world' ? 'Hello Vue' : 'Hello world'
      }
    }
  }
</script>

在 data 中添加 info 变量,点击 Add age 按钮修改 this.info.age 时,是否触发了子组件 props 的变化?子组件为什么会重新渲染?

本文的相关代码在源代码中的位置如下: packages/runtime-core/src/componentProps.ts packages/reactivity/src/reactive.ts packages/reactivity/src/baseHandlers.ts