Appearance
第5.3节:Props 与 Emits:组件的外部接口是如何工作的?
Props 和 Emits 是 Vue.js 组件与外部世界通信的核心机制。Props 负责接收父组件传递的数据,而 Emits 负责向父组件发送事件。本节将深入分析这两个系统的源码实现,揭示它们如何协同工作来构建组件的外部接口。
Props 系统深度解析
1. Props 定义:类型检查和默认值
Props 选项的类型定义
在 componentProps.ts 中,Vue 定义了完整的 Props 类型系统:
typescript
// Props 选项的基础类型
export interface PropOptions<T = any, D = T> {
type?: PropType<T> | true | null
required?: boolean
default?: D | DefaultFactory<D> | null | undefined | object
validator?(value: unknown, props: Data): boolean
skipCheck?: boolean
skipFactory?: boolean
}
// Props 类型构造器
export type PropType<T> = PropConstructor<T> | (PropConstructor<T> | null)[]
type PropConstructor<T = any> =
| { new (...args: any[]): T & {} }
| { (): T }
| PropMethod<T>这个类型系统支持:
- 类型检查:通过
type字段指定期望的数据类型 - 必需性检查:通过
required字段标记必需的 props - 默认值:通过
default字段提供默认值或默认值工厂函数 - 自定义验证:通过
validator函数进行复杂的验证逻辑
Props 规范化处理
normalizePropsOptions 函数负责将用户定义的 props 选项规范化为内部使用的格式:
typescript
export function normalizePropsOptions(
comp: ConcreteComponent,
appContext: AppContext,
asMixin = false,
): NormalizedPropsOptions {
const cache = appContext.propsCache
const cached = cache.get(comp)
if (cached) {
return cached
}
const raw = comp.props
const normalized: NormalizedProps = {}
const needCastKeys: string[] = []
// 处理继承和混入的 props
let hasExtends = false
if (!isFunction(comp)) {
const extendProps = (raw: ComponentOptions) => {
if (raw.props) {
hasExtends = true
const [props, keys] = normalizePropsOptions(raw, appContext, true)
extend(normalized, props)
if (keys) needCastKeys.push(...keys)
}
}
// 处理 mixins
if (!asMixin && appContext.mixins.length) {
appContext.mixins.forEach(extendProps)
}
// 处理 extends
if (comp.extends) {
extendProps(comp.extends)
}
// 处理组件自身的 mixins
if (comp.mixins) {
comp.mixins.forEach(extendProps)
}
}
// 规范化 props 定义
if (raw) {
if (isArray(raw)) {
// 数组形式:['prop1', 'prop2']
for (let i = 0; i < raw.length; i++) {
const normalizedKey = camelize(raw[i])
if (validatePropName(normalizedKey)) {
normalized[normalizedKey] = EMPTY_OBJ
}
}
} else {
// 对象形式:{ prop1: Type, prop2: { type: Type, default: value } }
for (const key in raw) {
const normalizedKey = camelize(key)
if (validatePropName(normalizedKey)) {
const opt = raw[key]
const prop: NormalizedProp = (normalized[normalizedKey] =
isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
// 处理布尔类型的特殊转换
if (prop.type) {
const booleanIndex = getTypeIndex(Boolean, prop.type)
const stringIndex = getTypeIndex(String, prop.type)
prop[BooleanFlags.shouldCast] = booleanIndex > -1
prop[BooleanFlags.shouldCastTrue] =
stringIndex < 0 || booleanIndex < stringIndex
if (booleanIndex > -1 || hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey)
}
}
}
}
}
}
const res: NormalizedPropsOptions = [normalized, needCastKeys]
cache.set(comp, res)
return res
}2. Props 传递:父子组件数据流
Props 初始化
initProps 函数在组件实例创建时初始化 props:
typescript
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
isStateful: number,
isSSR = false,
): void {
const props: Data = {}
const attrs: Data = {}
// 创建内部对象标记
createInternalObject(attrs)
instance.propsDefaults = Object.create(null)
// 设置完整的 props 和 attrs
setFullProps(instance, rawProps, props, attrs)
// 验证必需的 props
for (const key in instance.propsOptions[0]) {
if (!(key in props)) {
props[key] = undefined
}
}
// 开发环境下的验证
if (__DEV__) {
validateProps(rawProps || {}, props, instance)
}
if (isStateful) {
// 有状态组件:使用 shallowReactive 包装 props
instance.props = isSSR ? props : shallowReactive(props)
} else {
// 函数式组件:直接使用 props 或 attrs
if (!instance.type.props) {
instance.props = attrs
} else {
instance.props = props
}
}
// attrs 使用 shallowReactive 包装
instance.attrs = shallowReactive(attrs)
}Props 和 Attrs 的分离
setFullProps 函数负责将传入的原始 props 分离为声明的 props 和未声明的 attrs:
typescript
function setFullProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
props: Data,
attrs: Data,
) {
const [options, needCastKeys] = instance.propsOptions
let hasAttrsChanged = false
let rawCastValues: Data | undefined
if (rawProps) {
for (let key in rawProps) {
// 跳过保留属性
if (isReservedProp(key)) {
continue
}
const value = rawProps[key]
let camelKey
// 检查是否为声明的 prop
if (options && hasOwn(options, (camelKey = camelize(key)))) {
if (!needCastKeys || !needCastKeys.includes(camelKey)) {
props[camelKey] = value
} else {
// 需要类型转换的 prop
;(rawCastValues || (rawCastValues = {}))[camelKey] = value
}
} else if (!isEmitListener(instance.emitsOptions, key)) {
// 未声明的属性放入 attrs
if (!(key in attrs) || value !== attrs[key]) {
attrs[key] = value
hasAttrsChanged = true
}
}
}
}
// 处理需要类型转换的 props
if (needCastKeys) {
const rawCurrentProps = toRaw(props)
const castValues = rawCastValues || EMPTY_OBJ
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
props[key] = resolvePropValue(
options!,
rawCurrentProps,
key,
castValues[key],
instance,
!hasOwn(castValues, key),
)
}
}
return hasAttrsChanged
}3. Props 验证:运行时类型检查
默认值解析
resolvePropValue 函数处理 prop 的默认值和类型转换:
typescript
function resolvePropValue(
options: NormalizedProps,
props: Data,
key: string,
value: unknown,
instance: ComponentInternalInstance,
isAbsent: boolean,
) {
const opt = options[key]
if (opt != null) {
const hasDefault = hasOwn(opt, 'default')
// 处理默认值
if (hasDefault && value === undefined) {
const defaultValue = opt.default
if (
opt.type !== Function &&
!opt.skipFactory &&
isFunction(defaultValue)
) {
// 默认值工厂函数
const { propsDefaults } = instance
if (key in propsDefaults) {
value = propsDefaults[key]
} else {
const reset = setCurrentInstance(instance)
value = propsDefaults[key] = defaultValue.call(
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
? createPropsDefaultThis(instance, props, key)
: null,
props,
)
reset()
}
} else {
value = defaultValue
}
}
// 布尔类型转换
if (opt[BooleanFlags.shouldCast]) {
if (isAbsent && !hasDefault) {
value = false
} else if (
opt[BooleanFlags.shouldCastTrue] &&
(value === '' || value === hyphenate(key))
) {
value = true
}
}
}
return value
}运行时验证
开发环境下,Vue 会对 props 进行严格的类型和自定义验证:
typescript
function validateProps(
rawProps: Data,
props: Data,
instance: ComponentInternalInstance,
) {
const resolvedValues = toRaw(props)
const options = instance.propsOptions[0]
for (const key in options) {
let opt = options[key]
if (opt == null) continue
validateProp(
key,
resolvedValues[key],
opt,
resolvedValues,
!hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key))
)
}
}
function validateProp(
name: string,
value: unknown,
prop: PropOptions,
props: Data,
isAbsent: boolean,
) {
const { type, required, validator } = prop
// 必需性检查
if (required && isAbsent) {
warn('Missing required prop: "' + name + '"')
return
}
// 类型检查
if (value == null && !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, props)) {
warn('Invalid prop: custom validator check failed for prop "' + name + '".')
}
}4. Props 更新:响应式 Props 的处理
Props 更新机制
updateProps 函数处理 props 的更新,支持优化的部分更新和完整更新:
typescript
export function updateProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
rawPrevProps: Data | null,
optimized: boolean,
): void {
const {
props,
attrs,
vnode: { patchFlag },
} = instance
const rawCurrentProps = toRaw(props)
const [options] = instance.propsOptions
let hasAttrsChanged = false
if (
(optimized || patchFlag > 0) &&
!(patchFlag & PatchFlags.FULL_PROPS)
) {
if (patchFlag & PatchFlags.PROPS) {
// 编译器生成的优化更新:只更新动态 props
const propsToUpdate = instance.vnode.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
let key = propsToUpdate[i]
// 跳过事件监听器
if (isEmitListener(instance.emitsOptions, key)) {
continue
}
const value = rawProps![key]
if (options) {
if (hasOwn(attrs, key)) {
// 更新 attrs
if (value !== attrs[key]) {
attrs[key] = value
hasAttrsChanged = true
}
} else {
// 更新 props
const camelizedKey = camelize(key)
props[camelizedKey] = resolvePropValue(
options,
rawCurrentProps,
camelizedKey,
value,
instance,
false,
)
}
} else {
// 函数式组件:直接更新 attrs
if (value !== attrs[key]) {
attrs[key] = value
hasAttrsChanged = true
}
}
}
}
} else {
// 完整更新
if (setFullProps(instance, rawProps, props, attrs)) {
hasAttrsChanged = true
}
// 删除不再存在的 props
let kebabKey: string
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,
rawCurrentProps,
key,
undefined,
instance,
true,
)
}
} else {
delete props[key]
}
}
}
// 清理不再存在的 attrs
if (attrs !== rawCurrentProps) {
for (const key in attrs) {
if (!rawProps || !hasOwn(rawProps, key)) {
delete attrs[key]
hasAttrsChanged = true
}
}
}
}
// 触发 $attrs 的响应式更新
if (hasAttrsChanged) {
trigger(instance.attrs, TriggerOpTypes.SET, '')
}
// 开发环境验证
if (__DEV__) {
validateProps(rawProps || {}, props, instance)
}
}Emits 系统深度解析
1. 事件定义:Emits 选项
Emits 类型系统
在 componentEmits.ts 中,Vue 定义了完整的事件类型系统:
typescript
// 对象形式的 emits 选项
export type ObjectEmitsOptions = Record<
string,
((...args: any[]) => any) | null
>
// emits 选项:数组或对象形式
export type EmitsOptions = ObjectEmitsOptions | string[]
// 将 emits 转换为 props 类型
export type EmitsToProps<T extends EmitsOptions> =
T extends string[]
? {
[K in `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
}
: T extends ObjectEmitsOptions
? {
[K in string & keyof T as `on${Capitalize<K>}`]?: (
...args: T[K] extends (...args: infer P) => any
? P
: T[K] extends null
? any[]
: never
) => any
}
: {}
// emit 函数类型
export type EmitFn<
Options = ObjectEmitsOptions,
Event extends keyof Options = keyof Options,
> =
Options extends Array<infer V>
? (event: V, ...args: any[]) => void
: {} extends Options
? (event: string, ...args: any[]) => void
: UnionToIntersection<
{
[key in Event]: Options[key] extends (...args: infer Args) => any
? (event: key, ...args: Args) => void
: Options[key] extends any[]
? (event: key, ...args: Options[key]) => void
: (event: key, ...args: any[]) => void
}[Event]
>Emits 选项规范化
normalizeEmitsOptions 函数将用户定义的 emits 选项规范化:
typescript
export function normalizeEmitsOptions(
comp: ConcreteComponent,
appContext: AppContext,
asMixin = false,
): ObjectEmitsOptions | null {
const cache = appContext.emitsCache
const cached = cache.get(comp)
if (cached !== undefined) {
return cached
}
const raw = comp.emits
let normalized: ObjectEmitsOptions = {}
// 处理继承和混入的 emits
let hasExtends = false
if (!isFunction(comp)) {
const extendEmits = (raw: ComponentOptions) => {
const normalizedFromExtend = normalizeEmitsOptions(raw, appContext, true)
if (normalizedFromExtend) {
hasExtends = true
extend(normalized, normalizedFromExtend)
}
}
// 处理 mixins
if (!asMixin && appContext.mixins.length) {
appContext.mixins.forEach(extendEmits)
}
// 处理 extends
if (comp.extends) {
extendEmits(comp.extends)
}
// 处理组件自身的 mixins
if (comp.mixins) {
comp.mixins.forEach(extendEmits)
}
}
if (!raw && !hasExtends) {
if (isObject(comp)) {
cache.set(comp, null)
}
return null
}
if (isArray(raw)) {
// 数组形式:['event1', 'event2']
raw.forEach(key => (normalized[key] = null))
} else {
// 对象形式:{ event1: validator, event2: null }
extend(normalized, raw)
}
if (isObject(comp)) {
cache.set(comp, normalized)
}
return normalized
}2. 事件触发:$emit 的实现
核心 emit 函数
emit 函数是事件触发的核心实现:
typescript
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
): ComponentPublicInstance | null | undefined {
// 检查组件是否已卸载
if (instance.isUnmounted) return
const props = instance.vnode.props || EMPTY_OBJ
// 开发环境下的验证
if (__DEV__) {
const {
emitsOptions,
propsOptions: [propsOptions],
} = instance
if (emitsOptions) {
if (!(event in emitsOptions)) {
if (!propsOptions || !(toHandlerKey(camelize(event)) in propsOptions)) {
warn(
`Component emitted event "${event}" but it is neither declared in ` +
`the emits option nor as an "${toHandlerKey(camelize(event))}" prop.`,
)
}
} else {
// 执行事件验证器
const validator = emitsOptions[event]
if (isFunction(validator)) {
const isValid = validator(...rawArgs)
if (!isValid) {
warn(
`Invalid event arguments: event validation failed for event "${event}".`,
)
}
}
}
}
}
let args = rawArgs
// 处理 v-model 相关的修饰符
const isModelListener = event.startsWith('update:')
const modifiers = isModelListener && getModelModifiers(props, event.slice(7))
if (modifiers) {
if (modifiers.trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
}
if (modifiers.number) {
args = rawArgs.map(looseToNumber)
}
}
// 开发工具支持
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentEmit(instance, event, args)
}
// 查找事件处理器
let handlerName
let handler =
props[(handlerName = toHandlerKey(event))] ||
props[(handlerName = toHandlerKey(camelize(event)))]
// 对于 v-model 事件,也尝试 kebab-case 形式
if (!handler && isModelListener) {
handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
}
// 执行事件处理器
if (handler) {
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args,
)
}
// 处理 .once 修饰符
const onceHandler = props[handlerName + `Once`]
if (onceHandler) {
if (!instance.emitted) {
instance.emitted = {}
} else if (instance.emitted[handlerName]) {
return
}
instance.emitted[handlerName] = true
callWithAsyncErrorHandling(
onceHandler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args,
)
}
}3. 事件验证:参数类型检查
事件监听器识别
isEmitListener 函数用于识别一个 prop 是否为事件监听器:
typescript
export function isEmitListener(
options: ObjectEmitsOptions | null,
key: string,
): boolean {
if (!options || !isOn(key)) {
return false
}
// 移除 'on' 前缀和 'Once' 后缀
key = key.slice(2).replace(/Once$/, '')
return (
hasOwn(options, key[0].toLowerCase() + key.slice(1)) ||
hasOwn(options, hyphenate(key)) ||
hasOwn(options, key)
)
}事件参数验证
在 emit 函数中,如果定义了事件验证器,会在触发事件时进行参数验证:
typescript
// 在 emit 函数中的验证逻辑
if (emitsOptions) {
if (event in emitsOptions) {
const validator = emitsOptions[event]
if (isFunction(validator)) {
const isValid = validator(...rawArgs)
if (!isValid) {
warn(
`Invalid event arguments: event validation failed for event "${event}".`,
)
}
}
}
}4. v-model:双向绑定的实现
v-model 的工作原理
v-model 是 Vue 中双向绑定的语法糖,它实际上是 prop 和 event 的组合:
typescript
// v-model="value" 等价于:
// :model-value="value" @update:model-value="value = $event"
// 在 emit 函数中处理 v-model 相关逻辑
const isModelListener = event.startsWith('update:')
const modifiers = isModelListener && getModelModifiers(props, event.slice(7))
if (modifiers) {
// 处理 .trim 修饰符
if (modifiers.trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
}
// 处理 .number 修饰符
if (modifiers.number) {
args = rawArgs.map(looseToNumber)
}
}defineModel 宏的实现
在 apiSetupHelpers.ts 中,Vue 3.4+ 提供了 defineModel 宏来简化 v-model 的使用:
typescript
export type ModelRef<T, M extends PropertyKey = string, G = T, S = T> = Ref<
G,
S
> &
[ModelRef<T, M, G, S>, Record<M, true | undefined>]
export type DefineModelOptions<T = any, G = T, S = T> = {
get?: (v: T) => G
set?: (v: S) => any
}
/**
* Vue `<script setup>` 编译器宏,用于声明可以通过 v-model 消费的双向绑定 prop
* 这将声明一个同名的 prop 和对应的 `update:propName` 事件
*/
export function defineModel<T>(
name?: string,
options?: DefineModelOptions<T>
): ModelRef<T> {
if (__DEV__) {
warnRuntimeUsage(`defineModel`)
}
return null as any
}实际应用场景
1. 复杂表单组件
typescript
// 表单输入组件
const FormInput = defineComponent({
props: {
modelValue: {
type: String,
required: true
},
placeholder: {
type: String,
default: ''
},
rules: {
type: Array as PropType<ValidationRule[]>,
default: () => []
}
},
emits: {
'update:modelValue': (value: string) => typeof value === 'string',
'validation-error': (errors: string[]) => Array.isArray(errors)
},
setup(props, { emit }) {
const validate = (value: string) => {
const errors = props.rules
.map(rule => rule(value))
.filter(Boolean)
if (errors.length > 0) {
emit('validation-error', errors)
return false
}
return true
}
const handleInput = (event: Event) => {
const value = (event.target as HTMLInputElement).value
if (validate(value)) {
emit('update:modelValue', value)
}
}
return { handleInput }
}
})2. 自定义组件库
typescript
// 按钮组件
const Button = defineComponent({
props: {
type: {
type: String as PropType<'primary' | 'secondary' | 'danger'>,
default: 'primary',
validator: (value: string) => ['primary', 'secondary', 'danger'].includes(value)
},
size: {
type: String as PropType<'small' | 'medium' | 'large'>,
default: 'medium'
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
}
},
emits: {
click: (event: MouseEvent) => event instanceof MouseEvent,
focus: (event: FocusEvent) => event instanceof FocusEvent,
blur: (event: FocusEvent) => event instanceof FocusEvent
},
setup(props, { emit }) {
const handleClick = (event: MouseEvent) => {
if (!props.disabled && !props.loading) {
emit('click', event)
}
}
return { handleClick }
}
})性能优化策略
1. Props 优化
typescript
// 使用 shallowRef 优化大对象 props
const props = defineProps<{
largeData: Record<string, any>
}>()
// 在父组件中使用 shallowRef
const largeData = shallowRef({
// 大量数据
})2. 事件优化
typescript
// 避免在模板中创建内联函数
// 不好的做法
<button @click="() => handleClick(item.id)">Click</button>
// 好的做法
<button @click="handleClick" :data-id="item.id">Click</button>
const handleClick = (event: MouseEvent) => {
const id = (event.target as HTMLElement).dataset.id
// 处理逻辑
}3. 编译时优化
Vue 编译器会对 props 和 emits 进行多种优化:
- 静态提升:将静态 props 提升到渲染函数外部
- 补丁标记:为动态 props 添加补丁标记,实现精确更新
- 事件缓存:缓存事件处理器,避免重复创建
总结
Props 和 Emits 系统是 Vue.js 组件通信的核心机制。通过深入分析源码,我们了解到:
- Props 系统提供了完整的类型检查、默认值处理、验证机制和响应式更新
- Emits 系统实现了类型安全的事件触发和参数验证
- v-model 通过 props 和 emits 的组合实现了优雅的双向绑定
- 性能优化通过编译时分析和运行时优化确保了高效的组件通信
这些机制共同构成了 Vue.js 强大而灵活的组件系统,为开发者提供了类型安全、性能优异的组件通信方案。
