本文基于 Vue3 +
<script setup>语法,用最通俗的语言 + 可直接运行的代码示例,彻底讲透 Vue3 最核心的组件通信方式。读完即可直接应用到项目中,解决 90% 的组件数据交互问题。
文章目录
-
- 前言
- 一、核心原则:单向数据流
- 二、父传子:向下传递数据
-
- [1. 核心原理](#1. 核心原理)
- [2. 使用规则](#2. 使用规则)
- [3. defineProps 三种写法](#3. defineProps 三种写法)
-
- 写法1:数组最简写法(仅声明,无校验)
- [写法2:对象写法(带类型校验 + 默认值 + 必填校验)](#写法2:对象写法(带类型校验 + 默认值 + 必填校验))
- [写法3:TS 泛型写法(TS 项目专属)](#写法3:TS 泛型写法(TS 项目专属))
- [4. 完整代码示例](#4. 完整代码示例)
- [5. 易错点提醒](#5. 易错点提醒)
- 三、子传父:向上触发事件
-
- [1. 核心原理](#1. 核心原理)
- [2. 使用规则](#2. 使用规则)
- [3. 完整代码示例](#3. 完整代码示例)
- [4. 易错点提醒](#4. 易错点提醒)
- 四、爷孙隔代传值:层层透传
-
- [1. 核心原理](#1. 核心原理)
- [2. 使用规则](#2. 使用规则)
- [3. 完整代码示例](#3. 完整代码示例)
- [4. 弊端分析](#4. 弊端分析)
- 五、通信方案对比与最佳实践
- 总结
前言
Vue 最核心的思想就是组件化开发 :把一个复杂的页面拆分成多个独立、可复用的组件。但组件之间不是孤立的,它们需要互相传递数据、触发事件,这就是组件通信。
很多初学者在学习组件通信时容易混淆概念,尤其是子传父和隔代传值。本文将从最基础的父子通信讲起,逐步深入到爷孙隔代传值,每个知识点都配有原理剖析 + 明确规则 + 完整可运行代码 + 易错点提醒,保证你看完就能用。
一、核心原则:单向数据流
在开始讲解具体通信方式之前,必须先记住 Vue 组件通信的第一铁律:
数据永远单向向下流动,事件永远向上冒泡。
- 父组件的数据只能向下传给子组件
- 子组件不能直接修改父组件的数据
- 子组件想修改父组件的数据,只能通过触发事件通知父组件自己修改
这个原则是所有 Vue 组件通信的基础,违背它会导致数据混乱、难以调试。
二、父传子:向下传递数据
1. 核心原理
父组件拥有数据,通过标签属性 的方式将数据传递给子组件;子组件通过 defineProps 编译宏接收数据,并且只能读取,不能修改。
通俗比喻:爸爸有 100 块钱,直接递给儿子用,儿子只能花,不能偷偷把爸爸的钱改成 200。
2. 使用规则
- 数据定义在父组件中
- 父组件在子组件标签上通过
:属性名="数据"传递(静态字符串可省略:) - 子组件使用
defineProps()接收数据 - 子组件只能读取数据,绝对不能直接修改 props
3. defineProps 三种写法
写法1:数组最简写法(仅声明,无校验)
适合快速开发、简单场景,不推荐在企业级项目中使用。
vue
<script setup>
// 直接声明要接收的属性名
const props = defineProps(['money', 'name'])
</script>
写法2:对象写法(带类型校验 + 默认值 + 必填校验)
企业级项目首选,能有效减少数据类型错误。
vue
<script setup>
const props = defineProps({
name: {
type: String, // 限定数据类型
required: true // 标记为必传属性
},
money: {
type: Number,
default: 0 // 不传时的默认值
},
goods: {
type: Array,
// 对象/数组的默认值必须用函数返回
default: () => []
}
})
</script>
写法3:TS 泛型写法(TS 项目专属)
配合 TypeScript 使用,提供更强大的类型提示和校验。
vue
<script setup lang="ts">
interface Props {
name: string
money?: number // ? 表示非必填
goods?: number[]
}
const props = defineProps<Props>()
</script>
4. 完整代码示例
父组件:Parent.vue
vue
<template>
<div class="parent">
<h2>我是爸爸</h2>
<p>爸爸的总资产:{{ totalMoney }}</p>
<hr />
<!-- 父传子:静态传值 + 动态传值 -->
<Son name="小明" :money="totalMoney" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Son from './Son.vue'
// 父组件的数据
const totalMoney = ref(100)
</script>
子组件:Son.vue
vue
<template>
<div class="son">
<h3>我是儿子</h3>
<p>我的名字:{{ name }}</p>
<p>爸爸给我的钱:{{ money }}</p>
</div>
</template>
<script setup lang="ts">
// 接收父组件传来的数据
const props = defineProps<{
name: string
money: number
}>()
// ❌ 错误:不能直接修改 props
// props.money = 200
</script>
5. 易错点提醒
- 静态传值(字符串)可以省略
:,动态传值(变量、表达式、对象)必须加: - 子组件中,模板可以直接使用属性名,JS 代码中必须通过
props.属性名访问 - 绝对不能直接修改 props,否则会触发 Vue 警告
三、子传父:向上触发事件
1. 核心原理
子组件不能直接修改父组件的数据,只能通过触发自定义事件的方式通知父组件;父组件监听这个事件,在自己的回调函数中修改数据。
通俗比喻:儿子考了 100 分,不能直接把分数塞给爸爸,只能大喊一声"我考了100分!",爸爸听到后自己记录分数。
2. 使用规则
- 子组件使用
defineEmits()定义要触发的事件名 - 子组件通过
emit('事件名', 数据)触发事件并传递数据 - 父组件在子组件标签上通过
@事件名="回调函数"监听事件 - 父组件在回调函数中接收数据并修改自己的状态
3. 完整代码示例
子组件:Son.vue
vue
<template>
<div class="son">
<h3>我是儿子</h3>
<p>爸爸给我的钱:{{ money }}</p>
<button @click="giveMoney">给爸爸 20 元</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
money: number
}>()
// 定义要触发的事件
const emit = defineEmits(['give-money'])
const giveMoney = () => {
// 触发事件,传递数据 20
emit('give-money', 20)
}
</script>
父组件:Parent.vue
vue
<template>
<div class="parent">
<h2>我是爸爸</h2>
<p>爸爸的总资产:{{ totalMoney }}</p>
<hr />
<!-- 监听子组件的事件 -->
<Son :money="totalMoney" @give-money="handleGetMoney" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Son from './Son.vue'
const totalMoney = ref(100)
// 处理子组件传来的数据
const handleGetMoney = (val: number) => {
// 父组件自己修改自己的数据
totalMoney.value -= val
}
</script>
4. 易错点提醒
- 事件名推荐使用短横线命名法 (如
give-money),符合 HTML 规范 emit可以传递多个参数:emit('event', arg1, arg2, arg3)- 父组件的回调函数会按顺序接收这些参数
四、爷孙隔代传值:层层透传
1. 核心原理
爷组件和孙组件不能直接通信,必须通过父组件(中间层) 中转。
- 爷传孙:爷 → 父 → 孙(一层层向下传 props)
- 孙传爷:孙 → 父 → 爷(一层层向上触发事件)
通俗比喻:爷爷要给孙子糖,必须先递给爸爸,爸爸再递给孙子;孙子要给爷爷写信,必须先交给爸爸,爸爸再转交给爷爷。
2. 使用规则
- 中间层(父组件)只做数据搬运工,不修改数据
- 向下传递:中间层用
defineProps接收,再通过属性传给下一层 - 向上传递:中间层用
@监听事件,再通过emit转发给上一层
3. 完整代码示例
爷组件:Grandpa.vue
vue
<template>
<div class="grandpa">
<h2>我是爷爷</h2>
<p>爷爷的总资产:{{ totalMoney }}</p>
<hr />
<!-- 爷传父 -->
<Father :money="totalMoney" @get-money="handleGetMoney" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Father from './Father.vue'
const totalMoney = ref(1000)
const handleGetMoney = (val: number) => {
totalMoney.value -= val
console.log('爷爷收到孙子给的:', val)
}
</script>
父组件:Father.vue(中间层,只做中转)
vue
<template>
<div class="father">
<h3>我是爸爸(中转)</h3>
<hr />
<!-- 父传孙 -->
<Son :money="money" @give-money="handleGiveMoney" />
</div>
</template>
<script setup lang="ts">
import Son from './Son.vue'
// 接收爷爷传来的数据
const props = defineProps<{
money: number
}>()
// 定义要转发给爷爷的事件
const emit = defineEmits(['get-money'])
const handleGiveMoney = (val: number) => {
// 转发事件给爷爷
emit('get-money', val)
}
</script>
孙组件:Son.vue
vue
<template>
<div class="son">
<h4>我是孙子</h4>
<p>收到爷爷的钱:{{ money }}</p>
<button @click="sendMoney">给爷爷 200 元</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
money: number
}>()
const emit = defineEmits(['give-money'])
const sendMoney = () => {
emit('give-money', 200)
}
</script>
4. 弊端分析
层层透传虽然简单,但当组件层级很深时,会出现**"props 地狱"**:
- 中间层组件会出现大量与自身业务无关的 props 和事件
- 代码冗余,维护困难
- 数据流向不清晰
解决方案:
- 简单跨级通信:使用
mitt事件总线 - 复杂全局状态:使用
Pinia状态管理
五、通信方案对比与最佳实践
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| props + emit | 父子组件通信 | 简单、规范、Vue 原生支持 | 只能父子通信,跨级需要层层透传 |
| mitt 事件总线 | 兄弟组件、简单跨级通信 | 轻量、简单、任意组件直接通信 | 不适合存储大量全局状态,需要手动取消订阅 |
| Pinia 状态管理 | 全局状态、复杂跨组件通信 | 集中管理、调试方便、TS 友好 | 有一定学习成本,简单场景使用过重 |
最佳实践
- 优先使用 props + emit:父子组件通信永远是首选,符合 Vue 设计规范
- 简单跨级用 mitt:兄弟组件、2-3 层隔代传值用 mitt 最省事
- 全局状态用 Pinia:用户信息、购物车、权限等全局共享数据必须用 Pinia
- 避免滥用事件总线:不要把所有通信都用 mitt 实现,否则会导致事件泛滥、难以维护
总结
本文详细讲解了 Vue3 中最常用的三种组件通信方式:
- 父传子 :用
defineProps接收,数据向下流动 - 子传父 :用
defineEmits触发事件,事件向上冒泡 - 爷孙隔代传值:通过中间层层层透传,复杂场景用 mitt 或 Pinia 替代
记住 Vue 组件通信的核心原则:数据向下,事件向上。只要遵循这个原则,你的组件代码就会清晰、易维护。