2026.01.20
组件属性传递 -> Props 与 Attrs 的设计与实现
在上一章中,我们实现了组件的挂载机制和响应式更新。然而,一个完整的组件系统离不开属性传递的支持。组件需要通过 props 接收外部数据,同时也需要处理未声明的 attributes。
在上一章中,我们实现了组件的挂载机制和响应式更新。然而,一个完整的组件系统离不开属性传递的支持。组件需要通过 props 接收外部数据,同时也需要处理未声明的 attributes。
本章将深入组件属性系统的设计与实现,理解 Vue 如何优雅地区分和处理 props 与 attrs。
问题场景
让我们先看一个典型的组件属性传递场景:
<div id="app"></div>
<script type="module">
import {h, createApp, ref} from '../dist/vue.esm.js'
const Comp = {
props: {
msg: String
},
setup(props, {attrs}) {
console.log('props:', props) // { msg: 'msg' }
console.log('attrs:', attrs) // { count: 0, a: 1 }
return {}
},
render() {
return h('div', 'hello component')
}
}
createApp(Comp, {msg: 'msg', count: 0, a: 1}).mount('#app')
</script>
核心问题:
当我们向组件传递 { msg: 'msg', count: 0, a: 1 } 时,Vue 如何区分哪些属性是 props,哪些是 attrs?
区分逻辑:
- Props:
{ msg: 'msg' }—— 组件中声明的属性 - Attrs:
{ count: 0, a: 1 }—— 未在组件中声明的属性
这种设计让组件能够:
- 明确接收声明的响应式数据(props)
- 灵活处理额外的 HTML 属性(attrs,如 class、style、data-* 等)
Props 的标准化处理
多种 Props 声明方式
Vue 支持两种 props 声明语法:
// 1. 对象语法(完整形式)
props: {
msg: String,
count: Number
}
// 2. 数组语法(简化形式)
props: ['msg', 'count']
为了统一处理,我们需要将数组形式标准化为对象形式。
实现 normalizePropsOptions
/**
* 标准化 props 选项
* @param props - 组件声明的 props(对象或数组)
* @returns 标准化后的 props 对象
*/
function normalizePropsOptions(props = {}) {
// 数组形式:['msg', 'count'] -> { msg: {}, count: {} }
if (isArray(props)) {
return props.reduce((prev, cur) => {
prev[cur] = {}
return prev
}, {})
}
// 对象形式直接返回
return props
}
在组件实例中保存 propsOptions
export function createComponentInstance(vnode) {
const { type } = vnode
const instance = {
type,
vnode,
render: null,
setupState: null,
propsOptions: normalizePropsOptions(type.props), // 保存标准化后的 props 声明
props: {}, // 实际接收的 props 值
attrs: {}, // 实际接收的 attrs 值
subTree: null,
isMounted: false
}
return instance
}
核心设计:
propsOptions:存储组件声明的 props 结构(作为"白名单")props:存储实际接收的 props 值(响应式对象)attrs:存储未声明的属性值(非响应式对象)
初始化 Props
在 setupComponent 中调用 initProps
现在我们需要在组件初始化时,将传入的属性分离为 props 和 attrs:
export function setupComponent(instance) {
const { type } = instance
// 初始化 props 和 attrs
initProps(instance)
// 执行 setup,并将 props 作为第一个参数传入
if (isFunction(type.setup)) {
const setupResult = proxyRefs(type.setup(instance.props, createSetupContext(instance)))
instance.setupState = setupResult
}
instance.render = type.render
}
/**
* 创建 setup 的第二个参数 context
*/
function createSetupContext(instance) {
return {
get attrs() {
return instance.attrs
}
}
}
实现 initProps
接下来实现 props 初始化的核心逻辑:
/**
* 初始化组件的 props 和 attrs
* @param instance - 组件实例
*/
function initProps(instance) {
const { vnode } = instance
const rawProps = vnode.props // 用户传递的所有属性
const props = {}
const attrs = {}
// 根据 propsOptions 分离 props 和 attrs
setFullProps(instance, rawProps, props, attrs)
// props 需要是响应式的,使用 reactive 包装
instance.props = reactive(props)
// attrs 不需要响应式
instance.attrs = attrs
}
核心设计:
- 从
vnode.props获取用户传递的所有属性 - 调用
setFullProps进行分离 - 重要:使用
reactive包装 props,使其具有响应式能力 - attrs 保持普通对象,不做响应式处理(性能优化)
实现 setFullProps
/**
* 将 rawProps 分离到 props 和 attrs
* @param instance - 组件实例
* @param rawProps - 用户传递的所有属性
* @param props - 分离后的 props 对象(引用传递)
* @param attrs - 分离后的 attrs 对象(引用传递)
*/
function setFullProps(instance, rawProps, props, attrs) {
const propsOptions = instance.propsOptions
if (rawProps) {
for (const key in rawProps) {
const value = rawProps[key]
// 判断是否在 propsOptions 中声明
if (hasOwn(propsOptions, key)) {
// 声明的属性放入 props
props[key] = value
}
else {
// 未声明的属性放入 attrs
attrs[key] = value
}
}
}
}
工作原理:
- 遍历用户传递的所有属性
- 使用
hasOwn(propsOptions, key)检查属性是否在组件中声明 - 声明的属性放入
props,未声明的放入attrs - 通过引用传递,直接修改
props和attrs对象
为什么 props 需要响应式?
// 父组件
const parentCount = ref(0)
createApp(Comp, { msg: parentCount.value })
// 子组件
setup(props) {
// props.msg 变化时,需要触发子组件重新渲染
watchEffect(() => {
console.log(props.msg)
})
}
代码重构:单一职责原则
为了保持代码的清晰和可维护性,我们将 props 相关的逻辑抽取到独立模块 componentProps.ts 中。
创建 componentProps.ts
在 packages/runtime-core/src/componentProps.ts 中:
import { reactive } from '@vue/reactivity'
import { hasOwn, isArray } from '@vue/shared'
/**
* 标准化 props 选项
* 将数组形式转换为对象形式
*/
export function normalizePropsOptions(props = {}) {
if (isArray(props)) {
return props.reduce((prev, cur) => {
prev[cur] = {}
return prev
}, {})
}
return props
}
/**
* 将 rawProps 分离到 props 和 attrs
*/
export function setFullProps(instance, rawProps, props, attrs) {
const propsOptions = instance.propsOptions
if (rawProps) {
for (const key in rawProps) {
const value = rawProps[key]
if (hasOwn(propsOptions, key)) {
props[key] = value
}
else {
attrs[key] = value
}
}
}
}
/**
* 初始化组件的 props 和 attrs
*/
export function initProps(instance) {
const { vnode } = instance
const rawProps = vnode.props
const props = {}
const attrs = {}
setFullProps(instance, rawProps, props, attrs)
// props 是响应式的,使用 reactive 包装
instance.props = reactive(props)
instance.attrs = attrs
}
模块职责:
normalizePropsOptions:标准化 props 声明setFullProps:分离 props 和 attrsinitProps:初始化组件的 props 和 attrs
更新 component.ts
在 packages/runtime-core/src/component.ts 中:
import { proxyRefs } from '@vue/reactivity'
import { isFunction } from '@vue/shared'
import { initProps, normalizePropsOptions } from './componentProps'
/**
* 创建组件实例
*/
export function createComponentInstance(vnode) {
const { type } = vnode
const instance = {
type,
vnode,
render: null, // 渲染函数
setupState: null, // setup 返回的状态
propsOptions: normalizePropsOptions(type.props), // 用户声明的组件 props
props: {}, // 实际接收的 props 值(响应式)
attrs: {}, // 实际接收的 attrs 值(非响应式)
subTree: null, // 子树(render 的返回值)
isMounted: false // 是否已挂载
}
return instance
}
/**
* 初始化组件
*/
export function setupComponent(instance) {
const { type } = instance
/**
* 初始化 props 和 attrs
*/
initProps(instance)
/**
* 执行 setup 函数
*/
const setupContext = createSetupContext(instance)
if (isFunction(type.setup)) {
const setupResult = proxyRefs(type.setup(instance.props, setupContext))
instance.setupState = setupResult
}
/**
* 绑定 render 函数
*/
instance.render = type.render
}
/**
* 创建 setup 的第二个参数 context
*/
function createSetupContext(instance) {
return {
get attrs() {
return instance.attrs
}
}
}
架构优势:
- 清晰的模块划分:props 处理逻辑独立于组件实例管理
- 易于测试:可以单独测试 props 的标准化和分离逻辑
- 可扩展性:后续添加 props 验证、默认值等特性时有明确的入口
总结
至此,我们完成了 Vue 组件属性系统的核心实现:
1. Props 标准化
- 支持对象和数组两种声明方式
- 使用
normalizePropsOptions统一为对象格式 - 将标准化结果保存在
instance.propsOptions中作为"白名单"
2. Props 与 Attrs 分离
- 通过
hasOwn(propsOptions, key)区分声明和未声明的属性 - 声明的属性进入
props(响应式对象) - 未声明的属性进入
attrs(普通对象)
3. 响应式设计
- Props 使用
reactive包装,支持响应式更新 - Attrs 保持普通对象,避免不必要的响应式开销
4. Setup Context
- 将
props作为 setup 的第一个参数传入 - 通过
context.attrs访问未声明的属性 - 使用 getter 确保访问到最新的 attrs 值
5. 单一职责原则
componentProps.ts:处理 props 的标准化、分离和初始化component.ts:处理组件实例的创建和初始化流程
这套机制为组件提供了灵活而强大的属性传递能力,既保证了类型安全(通过 props 声明),又支持透传未知属性(通过 attrs),是 Vue 组件系统的重要基础设施。