Skip to content

Props 与 Emits:组件的外部接口是如何工作的?

PropsEmits 是 Vue 组件的“外部接口”。它们共同定义了一个组件与父组件之间的“通信契约”。

  • Props:定义了父组件可以向子组件传递哪些数据(“下行”数据流)。
  • Emits:定义了子组件可以向父组件发送哪些通知(“上行”事件流)。

本节将深入源码,探究这份“契约”是如何被定义、验证和执行的。


Props:父向子的“下行契约”

Props 的处理流程分为四个阶段:规范化、初始化、验证和更新。

规范化 (normalizePropsOptions)

目标:将开发者定义的 props 选项(数组形式、对象形式等)统一转换为一种标准化的内部格式,便于机器读取。

开发者可以这样写:

javascript
// 写法 1:数组
props: ['title', 'likes']

// 写法 2:对象(类型)
props: { title: String, likes: Number }

// 写法 3:对象(完整选项)
props: {
  title: { type: String, required: true },
  likes: { type: Number, default: 0 }
}

normalizePropsOptions 函数会将这三种写法全部转换为一种统一的内部格式 NormalizedProps,并缓存结果:

javascript
// 规范化后的格式:
{
  title: { type: String, required: true },
  likes: { type: Number, default: 0 }
}

这个规范化过程在 appContext.propsCache 中完成,确保每个组件的 props 选项只需被解析一次。

初始化 (initProps)

目标:组件初始化时,根据“规范化”后的 props 选项,将父组件传入的原始数据分离props(已声明的)和 attrs(未声明的)。

当父组件渲染时,它传入了原始数据 rawProps<ChildComponent title="你好" :likes="10" class="card" data-id="123" />

initProps 函数会被调用,其核心是 setFullProps

typescript
// core/packages/runtime-core/src/componentProps.ts
export function initProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null,
  // ...
): void {
  const props: Data = {}
  const attrs: Data = {}
  
  // 【核心】将 rawProps 分离到 props 和 attrs
  setFullProps(instance, rawProps, props, attrs)

  // ...

  // 将 props 和 attrs 设为浅响应式
  instance.props = shallowReactive(props)
  instance.attrs = shallowReactive(attrs)
}

function setFullProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null,
  props: Data,
  attrs: Data,
) {
  // [options] 就是规范化后的 props 选项
  const [options, needCastKeys] = instance.propsOptions

  if (rawProps) {
    for (let key in rawProps) {
      // 跳过 Vue 内部保留属性
      if (isReservedProp(key)) continue

      const value = rawProps[key]
      const camelKey = camelize(key) // 'data-id' -> 'dataId'

      // 1. 检查:这个 key 是否在“已声明”的 (options) 中?
      if (options && hasOwn(options, camelKey)) {
        // 是 props -> 存入 props 对象
        props[camelKey] = value
      } else {
        // 2. 检查:这个 key 是不是 emits 监听器 (如 'onChange')?
        if (!isEmitListener(instance.emitsOptions, key)) {
          // 不是 props,也不是 listener -> 存入 attrs 对象
          attrs[key] = value
        }
      }
    }
  }
  // ... 还会处理默认值 (resolvePropValue)
}

initProps 完成后,instance.props 里只包含“已声明”的数据({ title, likes }),而 instance.attrs 里包含了所有“未声明”的数据({ class, data-id })。

验证 (validateProp)

目标:(仅限开发环境)在运行时检查父组件传入的数据是否符合 props 选项的定义。

initPropsupdateProps 的最后,__DEV__ 环境下会调用 validateProps

typescript
// core/packages/runtime-core/src/componentProps.ts
function validateProp(
  name: string,
  value: unknown,
  prop: PropOptions, // 该 prop 的定义
  isAbsent: boolean, // 父组件是否未传递
) {
  const { type, required, validator } = prop

  // 1. 检查“必需性”
  if (required && isAbsent) {
    warn('Missing required prop: "' + name + '"')
    return
  }

  // 2. 检查“类型”
  if (value == null && !required) {
    return // 未传值且非必需,跳过
  }
  if (type != null && type !== true) {
    // ... 调用 assertType 检查类型是否匹配 ...
    if (!isValid) {
      warn(getInvalidTypeMessage(name, value, expectedTypes))
      return
    }
  }
  
  // 3. 检查“自定义验证器”
  if (validator && !validator(value, props)) {
    warn('Invalid prop: custom validator check failed for prop "' + name + '".')
  }
}

4. 更新 (updateProps)

目标:当父组件重新渲染时,高效地更新子组件的 propsattrs

updateProps 会利用 transform 阶段生成的 patchFlag 来进行优化更新。

typescript
// core/packages/runtime-core/src/componentProps.ts
export function updateProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null, // 新的原始 props
  // ...
): void {
  const { props, attrs } = instance
  const { patchFlag } = instance.vnode // 获取编译时优化标记

  // 【优化路径】
  // 如果有 patchFlag 且不是“全量 props 对比”
  if ((patchFlag > 0) && !(patchFlag & PatchFlags.FULL_PROPS)) {
    // 检查是否只包含 PROPS (如 :id, :name)
    if (patchFlag & PatchFlags.PROPS) {
      // 编译器已提供了“动态 prop 列表”
      const propsToUpdate = instance.vnode.dynamicProps!
      
      // 只遍历“动态 prop 列表”,而不是“所有 props”
      for (let i = 0; i < propsToUpdate.length; i++) {
        const key = propsToUpdate[i]
        const value = rawProps![key]
        // ... (省略 props/attrs 分离和默认值处理)
        props[key] = value 
      }
    }
    // ... 还会单独处理 CLASS (2) 和 STYLE (4)
    
  } else {
    // 【全量路径】
    // 没有优化标记,或标记为 FULL_PROPS
    // 只能完整地重新执行 setFullProps
    setFullProps(instance, rawProps, props, attrs)
    // ... 并且需要遍历旧 props/attrs 来删除已不存在的
  }
  
  // ...
}

Emits:子向父的“上行契约”

Emits 的实现相对简单,其核心是“查找并执行一个 prop 回调”。

规范化 (normalizeEmitsOptions)

props 一样,emits 选项也需要规范化。

javascript
// 写法 1:数组
emits: ['change', 'delete']

// 写法 2:对象 (带验证器)
emits: {
  change: (id) => typeof id === 'number',
  delete: null // 允许
}

normalizeEmitsOptions 会将它们统一为 { change: Function | null, delete: null } 这种对象格式并缓存。

emit:触发“上行”通知

emits 契约的执行者是 emit 函数(即 setup 上下文中的 emit,或 this.$emit)。

typescript
// core/packages/runtime-core/src/componentEmits.ts
export function emit(
  instance: ComponentInternalInstance,
  event: string, // e.g., 'change'
  ...rawArgs: any[] // e.g., [123]
): void {
  // ...
  const props = instance.vnode.props || EMPTY_OBJ
  const { emitsOptions } = instance

  // 1. 【验证】(开发环境)
  if (__DEV__ && emitsOptions) {
    if (emitsOptions[event]) {
      // 执行开发者提供的验证器
      const validator = emitsOptions[event]
      if (isFunction(validator) && !validator(...rawArgs)) {
        warn(`Invalid event arguments: event validation failed...`)
      }
    }
  }

  // 2. 【查找】
  // 这是 emit 的核心:
  // 将“事件名” (event: 'change')
  // 转换为“Prop 回调名” (handlerName: 'onChange')
  let handlerName = toHandlerKey(camelize(event)) // 'change' -> 'onChange'
  let handler = props[handlerName]
  
  // 兼容 kebab-case
  if (!handler) {
    handler = props[toHandlerKey(hyphenate(event))] // 'onChange' -> 'on-change'
  }

  // 3. 【执行】
  // 如果父组件传递了 'onChange' prop,就执行它
  if (handler) {
    callWithAsyncErrorHandling(
      handler, // 父组件传入的函数
      instance,
      ErrorCodes.COMPONENT_EVENT_HANDLER,
      rawArgs // 传递参数
    )
  }
  
  // 4. (处理 .once 修饰符)
  // ...
}

emit('change', ...) 的本质就是去 props 列表里查找一个叫 onChange函数执行它。


v-model:“双向契约”的简写

v-modelPropsEmits 协同工作的简写形式。

  • <Child v-model="count" />

等价于:

  • <Child :modelValue="count" @update:modelValue="value => count = value" />
  1. Props (下行):子组件通过 props: ['modelValue'] 接收数据。
  2. Emits (上行):子组件在内部通过 emit('update:modelValue', newValue) 来通知父组件更新。

emit 函数会查找到 props['onUpdate:modelValue'](即父组件传入的箭头函数)并执行它,从而完成了 count = newValue 的赋值,实现“双向绑定”。


总结

PropsEmits 共同构建了组件的“通信契约”:

  1. 规范化:Vue 运行时首先将 props/emits 选项规范化为统一的内部格式并缓存。
  2. Props (下行)
    • initProps 在组件初始化时,将传入数据分离propsattrs
    • validateProp (开发环境) 负责验证数据是否符合 props 选项。
    • updateProps 负责在更新时,利用 PatchFlags 高效地应用新 props
  3. Emits (上行)
    • emit('foo') 函数的核心是**“查找并执行”**。它会自动查找父组件传入的 onFoo prop(即事件处理器)并执行它。

微信公众号二维码

Last updated: