Vue.js: 组件通信:props/emits

最后更新:2026-10-20

组件通信是 Vue 的核心机制——父组件用 props 传数据给子组件,子组件用 emits 触发事件通知父组件。理解双向数据流是写好 Vue 应用的关键。

Vue 3 的 <script setup> 提供了 defineProps 和 defineEmits 两个编译时宏,无需导入即可使用,且能完美支持 TypeScript 类型推断。

相关模板与辅助:useAttrs()

1. 你将学到


2. 一个电商购物车的"双向绑定"陷阱

(1) 痛点:子组件改 prop,父组件报错

Alice 实现了一个带数量输入的购物车:

Vue 报警告:

[Vue warn] Set operation on key "quantity" failed: target is readonly.

Charlie 调试 1 小时后发现:

"Alice,我改数量时父组件的购物车总价没更新。Vue 在修改 prop,但父组件不知道。"

(2) Vue 单向数据流 + emit 解法

现在数据流正确了:子组件 emit → 父组件 receive → 父组件更新 data → 重新传给子组件。

(3) 收益

修复数据流后:


3. props 完整语法

(1) 7 种 props 类型声明

VUE
<script setup>
defineProps({
  // 1. 基础类型
  name: String,
  age: Number,
  active: Boolean,
  
  // 2. 复杂类型
  user: Object,
  items: Array,
  
  // 3. 多类型(任一)
  id: [String, Number],
  
  // 4. 必填
  title: { type: String, required: true },
  
  // 5. 默认值
  pageSize: { type: Number, default: 20 },
  
  // 6. 自定义验证
  email: {
    type: String,
    validator: (val) => val.includes('@')
  },
  
  // 7. 函数默认值(默认值需要是函数)|
  createdAt: {
    type: Date,
    default: () => new Date()
  }
})
</script>

(2) 详细示例

VUE
<script setup>
defineProps({
  // 字符串
  title: String,
  
  // 数字 + 默认值 + 验证
  pageSize: {
    type: Number,
    default: 20,
    validator: (val) => val > 0 && val <= 100
  },
  
  // 布尔(注意:默认值的 falsy 值要用函数形式)
  isVip: {
    type: Boolean,
    default: false
  },
  
  // 数组默认值(必须用函数返回)
  tags: {
    type: Array,
    default: () => ['vue', 'javascript']
  },
  
  // 对象默认值(必须用函数返回)
  user: {
    type: Object,
    default: () => ({ name: 'Guest', age: 0 })
  },
  
  // 必填 + 自定义验证
  email: {
    type: String,
    required: true,
    validator: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
  }
})
</script>

(3) 6 大默认值规则

规则 示例
基本类型 default: 20
数组/对象 default: () => []
null default: null
undefined 不用写 default
函数引用 不用 default: fn,用 () => fn()
Symbol / BigInt 工厂函数返回

4. TypeScript 强类型 props(Vue 3 推荐)

(1) 基础类型声明

VUE
<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user'
}

defineProps<{
  user: User
  size?: 'small' | 'medium' | 'large'
  showEmail?: boolean
}>()
</script>

(2) 必填 vs 可选(?)

TS
defineProps<{
  // 必填(默认)
  user: User
  pageSize: number
  
  // 可选(加 ?)
  variant?: 'primary' | 'secondary'
  showIcon?: boolean
}>()

(3) withDefaults 提供默认值

VUE
<script setup lang="ts">
interface Props {
  user: User
  size?: 'small' | 'medium' | 'large'
  showEmail?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  size: 'medium',
  showEmail: true
})
</script>

(4) 运行时声明 vs 类型声明对比

维度 运行时 defineProps({}) 类型 defineProps<T>()
类型推断 弱 强(IDE 自动补全)
默认值 直接 default withDefaults 包装
验证 validator 函数 TypeScript 编译时检查
推荐 小项目/纯 JS 大项目/TS 项目

5. emit 完整语法

(1) 5 种事件声明

VUE
<script setup>
// 1. 简单声明
const emit = defineEmits(['click', 'submit', 'cancel'])

// 2. 带参数验证
const emit = defineEmits({
  addToCart: (productId) => typeof productId === 'number',
  remove: (id) => typeof id === 'string'
})

// 3. TypeScript 类型
const emit = defineEmits<{
  'add-to-cart': [productId: number]
  'remove-item': [id: string, reason: string]
  'update': [id: number, data: object]
}>()
</script>

(2) 5 种触发方式

VUE
<script setup>
const emit = defineEmits(['click', 'submit', 'add-to-cart'])

// 1. 简单触发
emit('click')

// 2. 传参
emit('add-to-cart', 123)

// 3. 传多个参数
emit('submit', { name: 'Alice' }, 2026)

// 4. 条件触发
if (isValid) emit('submit')

// 5. 函数式触发
function handleAdd() {
  emit('add-to-cart', props.product.id)
}
</script>

(3) 父组件监听

(4) emit 类型验证

VUE
<script setup>
const emit = defineEmits({
  // ✅ 验证:id 必须是 number
  remove: (id) => typeof id === 'number' || 'Validation failed',
  
  // ❌ 抛错:参数不匹配时
  // remove: 'invalid',  // 错:必须是函数
})
</script>

▶ 示例:v-model 双向绑定

▶ 试一试

输出:

TEXT
初始输入框显示占位符「搜索...」,下方文字「实时显示: (空)」。
输入 vue 时下方逐字同步,最终显示「实时显示: vue」;清空输入框又回到「(空)」。

6. 单向数据流原则

(1) 核心原则

100%
graph LR
    A[父组件 data] -->|props| B[子组件]
    B -->|emit| A
    
    style A fill:#42b883
    style B fill:#42b883

(2) 5 个例外情况

VUE
<script setup>
const props = defineProps({ user: Object })

// ❌ 错:直接修改 prop
props.user.name = 'Bob'  // Vue 警告!

// ✅ 例外 1:prop 是引用类型时,可以用新对象替换
// 父组件传的是引用,子组件不能改引用,但可以 emit 让父改

// ✅ 例外 2:prop 初始值可以保存到本地
const localUser = ref({ ...props.user })
// 然后修改 localUser(不影响父)

// ✅ 例外 3:用 computed 派生
const userName = computed(() => props.user.name)

// ✅ 例外 4:用 v-model(语法糖)
// <Child v-model="value" /> 等同于 :value + @update:value

// ✅ 例外 5:provide/inject 跨层级
// 父 provide,子 inject(Phase 2.5 学)
</script>

(3) 5 个反模式

JS
// ❌ 反模式 1:直接修改 prop
props.user.name = 'Bob'

// ❌ 反模式 2:在子组件用 watch 修改 prop
watch(() => props.value, (val) => { props.value = val * 2 })

// ❌ 反模式 3:双向绑定的 ref 透传(不推荐)
const localRef = ref(props.value)
watch(localRef, (val) => emit('update', val))

// ❌ 反模式 4:使用 v-model 时不遵循命名
// v-model 期望 update:xxx 事件,自定义事件名要配套
emit('change', val)  // ❌ 父组件 @change 不会触发
emit('update:value', val)  // ✅ v-model 配套

// ❌ 反模式 5:在子组件中直接管理父组件的数据
emit('update', { ...props.user, name: 'Bob' })  // 错:应该只传 ID,父组件自己更新

7. props 透传:inheritAttrs 和 useAttrs

(1) 默认行为

(2) 关闭自动透传

(3) useAttrs 显式使用

(4) 5 个使用场景

场景 做法
默认 不用管,class 自动应用到根
自定义根元素 useAttrs() 显式绑定
透传到非根元素 inheritAttrs: false + v-bind="$attrs"
多根组件(Fragment) 必须 inheritAttrs: false + 显式绑定
性能优化 关闭透传避免不必要的 DOM 属性

▶ 示例:useAttrs 透传(多根组件)

▶ 试一试

输出:

TEXT
显示标签「Name:」和一个输入框(占位符「Enter name...」,边框为绿色 .input-field),下方「输入值: 」为空。
输入 Alice 后,下方实时变为「输入值: Alice」——class 与 placeholder 都已通过 $attrs 绑到 input 上。

(1) 📊 5 个常见错误速查

错误 现象 解决
直接修改 prop Vue 警告 用 emit 让父改
数组/对象 prop 默认值是引用 多个组件共享 default: () => [] 工厂
v-model 事件名不匹配 父收不到 用 update:xxx
validator 返回字符串 Vue 不报错 返回 true/false
透传到多根组件 Vue 警告 inheritAttrs: false

(2) 📊 props 5 大验证场景

场景 验证
邮箱 val => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
URL val => /^https?:\/\//.test(val)
枚举值 val => ['a', 'b', 'c'].includes(val)
范围 val => val >= 0 && val <= 100
自定义对象 val => val.id && val.name

8. 完整示例:电商商品卡 props/emits

▶ 示例:ProductCard 完整 props/emits

▶ 展开运行示例(83 行)
▶ 试一试

输出:

TEXT
两张商品卡:iPhone 显示 $999 和可点击的「Add to Cart」;MacBook 显示 $2499、卡片半透明、按钮置灰并显示「Out of Stock」。
底部一行「购物车商品数: 0 | 收藏: 0」。
点击 iPhone 的 Add to Cart → 「购物车商品数: 1」;再点 ❤️ Favorite → 「收藏: 1」。

相关模板与辅助:useAttrs()

❓ 常见问题

Q props 能直接修改吗?
A 不能。Vue 单向数据流原则,props 只读。子组件要改数据必须 emit 事件让父组件改。直接修改会触发 Vue 警告。
Q 怎么传递复杂数据(嵌套对象)?
A 直接传引用,Vue 自动深响应。修改对象内部属性父组件能感知(深响应),但子组件不应直接修改,应 emit 让父改整个对象或属性。
Q defineProps 和 props 选项区别?
A <script setup> 中用 defineProps() 宏(无需 import),Options API 中用 props: {} 选项。两种等价,推荐 setup + defineProps。
Q emit 能在子组件中异步触发吗?
A 能。emit 是同步函数调用,但可以放在 setTimeout/Promise 中:setTimeout(() => emit('done'), 1000)。
Q v-model 的本质是什么?
A 是 props + emit 的语法糖。<Child v-model="x" /> 等同于 :modelValue="x" @update:modelValue="x = $event"。本课示例 3 详解。
Q 怎么在子组件中 watch props 变化?
A 用 watch(() => props.xxx, (newVal, oldVal) => {...})。注意不要在 watch 中修改 props,否则无限循环。
Q useAttrs 什么时候用?
A 3 个场景:(1) 多根组件(Fragment)必须用;(2) 想把 attribute 透传到非根元素;(3) 想要显式控制 attribute 绑定位置。普通单根组件不需要用。
Q props 验证失败会怎样?
A 开发环境 Vue 警告,生产环境静默失败。建议 validator 返回 false(错误)或返回字符串(错误消息)。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简单的 Button 组件:

    • props: text (String), variant (String, 'primary'/'success'/'danger'), disabled (Boolean)
    • 3 种 variant 对应 3 种 CSS class
    • emit click 事件(无参数)
    • 在父组件中测试 3 种 variant
  2. 进阶题(难度⭐⭐) 实现一个双向绑定的 Input 组件:

    • 子组件 CustomInput.vue:props: modelValue, label
    • 用 v-model 语法糖
    • emit update:modelValue
    • 父组件:实时显示用户输入
  3. 挑战题(难度⭐⭐⭐) 实现完整的"商品卡 + 收藏"系统:

    1. ProductCard.vue:完整 props(product/showStockBadge/maxStock)+ emits(add-to-cart/toggle-favorite)
    2. App.vue:商品列表 + 收藏夹状态
    3. 收藏按钮:emit toggle-favorite,父组件更新收藏状态
    4. 用 useAttrs + inheritAttrs 实现多根组件(不破坏练习)
    5. 至少 3 个 validator 验证(email/url/enum)
    6. TypeScript 强类型 props
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏