Skip to content

第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 组件通信的核心机制。通过深入分析源码,我们了解到:

  1. Props 系统提供了完整的类型检查、默认值处理、验证机制和响应式更新
  2. Emits 系统实现了类型安全的事件触发和参数验证
  3. v-model 通过 props 和 emits 的组合实现了优雅的双向绑定
  4. 性能优化通过编译时分析和运行时优化确保了高效的组件通信

这些机制共同构成了 Vue.js 强大而灵活的组件系统,为开发者提供了类型安全、性能优异的组件通信方案。


微信公众号二维码