Appearance
Props 与 Emits:组件的外部接口是如何工作的?
Props 和 Emits 是 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 选项的定义。
在 initProps 和 updateProps 的最后,__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)
目标:当父组件重新渲染时,高效地更新子组件的 props 和 attrs。
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-model 是 Props 和 Emits 协同工作的简写形式。
<Child v-model="count" />
等价于:
<Child :modelValue="count" @update:modelValue="value => count = value" />
- Props (下行):子组件通过
props: ['modelValue']接收数据。 - Emits (上行):子组件在内部通过
emit('update:modelValue', newValue)来通知父组件更新。
emit 函数会查找到 props['onUpdate:modelValue'](即父组件传入的箭头函数)并执行它,从而完成了 count = newValue 的赋值,实现“双向绑定”。
总结
Props 和 Emits 共同构建了组件的“通信契约”:
- 规范化:Vue 运行时首先将
props/emits选项规范化为统一的内部格式并缓存。 Props(下行):initProps在组件初始化时,将传入数据分离为props和attrs。validateProp(开发环境) 负责验证数据是否符合props选项。updateProps负责在更新时,利用PatchFlags高效地应用新props。
Emits(上行):emit('foo')函数的核心是**“查找并执行”**。它会自动查找父组件传入的onFooprop(即事件处理器)并执行它。
