Vue 组件通信全解

目录(快速导航)

  1. 父 → 子:Props(单向数据流)
  2. 子 → 父:事件(emit / defineEmits)与 v-model(双向语法糖)
  3. 父 调 子:ref + defineExpose()(调用子方法 / 访问实例)
  4. 兄弟组件通信:提升状态 / 父中转 / 共享状态(Pinia) / 事件总线
  5. 跨层级(祖孙/深层)通信:provide / inject(与响应性注意事项)
  6. 全局状态管理:Pinia(推荐用于大型或跨多层通信)
  7. 常见坑、性能与调试建议
  8. 最佳实践清单与速查表

1 父 → 子:Props(单向数据流)

核心要点: Props 是父组件向子组件传值的官方方式,遵循单向数据流 (父 → 子)。子组件收到的 props 只读(不能直接改),可声明类型、默认值、校验。

示例(<script setup>

js 复制代码
<!-- Child.vue -->
<script setup lang="ts">
// 定义 props(对象语法,推荐)
const props = defineProps({
  title: String,
  count: { type: Number, default: 0 },
  user: { type: Object, default: () => ({}) } // 对象/数组必须用工厂函数返回
})
</script>

<template>
  <!-- 使用 props -->
  <h2>{{ title }}</h2>
  <p>count: {{ count }}</p>
</template>

传递(父组件)

js 复制代码
<Child :title="post.title" :count="42" :user="{name:'张三'}" />
<!-- 或者绑定整个对象 -->
<Child v-bind="post" />

重要补充 --- Vue 3.5 vs 3.4 及以下(关于「解构 props」的响应性)

  • Vue 3.5+:

Vue 的响应系统基于属性访问跟踪状态的使用情况。例如,在计算属性或侦听器中访问 props.foo 时,foo 属性将被跟踪为依赖项。

因此,在以下代码的情况下:

js 复制代码
const { foo } = defineProps(['foo'])

watchEffect(() => {
  // 在 3.5 之前只运行一次
  // 在 3.5+ 中在 "foo" prop 变化时重新执行
  console.log(foo)
})
  • Vue 3.4 及以下:

在 3.4 及以下版本,foo 是一个实际的常量,永远不会改变。在 3.5 及以上版本,当在同一个 <script setup> 代码块中访问由 defineProps 解构的变量时,Vue 编译器会自动在前面添加 props.。因此,上面的代码等同于以下代码:

scss 复制代码
const props = defineProps(['foo'])

watchEffect(() => {
  // `foo` 由编译器转换为 `props.foo`
  console.log(props.foo)
})

2 子 → 父:emit / defineEmits 与

v-model

子组件不能直接改父组件数据,但可以通知父组件:常见方式是触发自定义事件(emit),父组件监听并处理。v-model 在组件上是 props + emit 的语法糖。

2.1 最基础:emit子组件

js 复制代码
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['ok', 'update']) // 声明可触发的事件(可选但推荐)
function onClick() {
  emit('ok', { msg: '子组件通知父组件' })
}
</script>

<template>
  <button @click="onClick">通知父组件</button>
</template>
父组件
js 复制代码
<Child @ok="handleOk" />
<script setup>
function handleOk(payload) {
  console.log('收到子组件的事件:', payload)
}
</script>

2.2 v-model(双向绑定语法糖)

  • 组件需要接受一个 prop(默认名 modelValue)并发出 update:modelValue 事件;父组件用 v-model 语法双向绑定。
子组件实现(标准)
js 复制代码
<!-- InputComp.vue -->
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])

function onInput(e) {
  emit('update:modelValue', e.target.value)
}
</script>

<template>
  <input :value="props.modelValue" @input="onInput" />
</template>
父组件使用
js 复制代码
<script setup>
import { ref } from 'vue'
const name = ref('')
</script>

<template>
  <InputComp v-model="name" />
</template>

2.3 自定义

v-model

名称与多个 v-model

  • v-model:propName 可以绑定到自定义 prop(需要子组件接收 propName 并 emit update:propName)。
示例(多个 v-model)
js 复制代码
<!-- Comp.vue -->
<script setup>
const props = defineProps({
  title: String,
  count: Number
})
const emit = defineEmits(['update:title','update:count'])
function setTitle(v){ emit('update:title', v) }
function setCount(v){ emit('update:count', v) }
</script>

父组件:

js 复制代码
<Comp v-model:title="t" v-model:count="n" />

TypeScript 中的写法(defineEmits的类型)

js 复制代码
const emit = defineEmits<{
  (e: 'update:modelValue', value: string): void
  (e: 'save', payload: {id:number}): void
}>()

3 父 调 子:ref+defineExpose()(直接调用子组件的方法)

有时父需要调用子组件方法或访问某些实例数据,子组件可以通过 defineExpose 暴露接口。

js 复制代码
<!-- Child.vue -->
<script setup>
function focusInput() { /* ... */ }
defineExpose({ focusInput })
</script>

父组件:

js 复制代码
<script setup>
import { ref } from 'vue'
const childRef = ref(null)
function callChild() { childRef.value.focusInput() }
</script>

<template>
  <Child ref="childRef" />
  <button @click="callChild">调用子方法</button>
</template>

注意: 这种方式打破封装性,应谨慎使用(适合组件库或必要的 API 暴露)。


4 兄弟组件通信

兄弟组件之间没有直接的 props / emit 通道,常见策略:

4.1 提升状态(Lift State Up)

把共享状态提升到最近的公共父组件,父持有状态并把数据通过 props 下发,兄弟通过 emit 通知父更新。

js 复制代码
SiblingA --(emit)--> Parent (update state) --(props)--> SiblingB

这是最清晰、最推荐的做法(可维护、可追踪)。

4.2 事件总线(不推荐用于大型项目,但小场景可用)

可以用轻量库 mitt 创建全局事件总线,任意组件 emit / on。但是会增加不可见的耦合和调试难度。

js 复制代码
// event-bus.js
import mitt from 'mitt'
export const bus = mitt()

使用:

js 复制代码
// 发射
bus.emit('user:change', user)
// 监听
bus.on('user:change', handler)

建议: 对于中大型项目优先使用 Pinia/全局状态或提升状态;仅在确实需要松耦合广播时使用事件总线。


5 跨层级(祖孙/深层)通信:provide/inject

provide/inject 适合跨越多层的依赖注入(例如插件、主题、父级服务等)。

基本用法

js 复制代码
<!-- Ancestor.vue -->
<script setup>
import { provide, reactive } from 'vue'
const state = reactive({ theme: 'dark' })
provide('appState', state) // 提供响应式对象
</script>
js 复制代码
<!-- DeepChild.vue -->
<script setup>
import { inject } from 'vue'
const appState = inject('appState')
</script>

响应性注意

  • 如果你 provide 一个普通对象(非 reactive/ref),子层 inject 到的就是普通对象,不会自动响应。
  • 推荐 provide ref/reactive/computed,或者提供 getter 函数以保证响应性:
js 复制代码
provide('count', toRef(state, 'count'))

场景适配

  • 适合:主题、国际化、表单上下文、可复用组件库内的共享配置。
  • 不适合:频繁变更且需要在多人组件之间频繁同步的大规模业务状态 ------ 这时用 Pinia 更合适。

6 全局状态管理:Pinia(推荐用于大型应用)

当状态需要被很多互不相邻的组件读写时,用 Pinia(或 Vuex)会更稳健、可追踪。

简单示例(Pinia)

js 复制代码
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
  state: () => ({ name: '', loggedIn: false }),
  actions: {
    login(name) { this.name = name; this.loggedIn = true }
  }
})

组件中使用:

js 复制代码
<script setup>
import { useUserStore } from '@/stores/user'
const user = useUserStore()
user.login('张三')
</script>

优点: 单一来源(single source of truth)、DevTools 支持、易于测试与追踪。


7 常见坑、性能与调试建议

常见坑

  • 修改 props:子组件不要直接修改 props。若需要本地编辑,复制一份到 ref/reactive。

  • 解构 props(版本问题) :3.4 及以下解构会丢失响应性(参见上文)。

  • 对象默认值:必须用工厂函数。

  • provide/inject 非响应性误区:记得提供 ref/reactive 保持响应性。

  • 过度使用事件总线:会让依赖关系隐式化,调试困难。

性能考虑

  • 频繁 emit/事件回流会带来性能开销,避免在短时间内大量事件通信(可合并、去抖)。

  • 对大对象频繁深拷贝会影响性能,尽量传引用或只传必要字段。

  • 存储大数据建议放在 Pinia/store 并通过 getters/computed 精细化访问。

调试技巧

  • 使用 Vue DevTools:查看组件树、props、state、pinia store 的 mutation 历史。
  • 控制台警告会指出非法 prop 修改、校验失败、未声明的 emits 等。
  • 在开发阶段为组件声明 defineEmits 与 defineProps 的类型,可更早发现问题。

8 最佳实践清单(可直接复制到团队规范)

  • Props:总是显式声明并优先使用对象语法或 TypeScript。
  • Props 默认值:对象/数组必须使用工厂函数返回。
  • 解构 props:考虑兼容性(3.5+ 可直接解构;3.4 及以下用 toRefs)。
  • 子→父:优先使用 emit,复杂共享状态使用 Pinia。
  • 双向绑定:优先使用 v-model(组件层用 update:xxx 事件)。
  • 兄弟通信:优先提升状态到父组件;只有确实必要时才用事件总线。
  • 跨层级共享:provide/inject 用于配置/服务类数据;应传递 ref/reactive 保持响应性。
  • 暴露 API:若使用 defineExpose,明确文档并限量暴露。
  • 命名规范:事件名使用 kebab-case(update:modelValue),prop 使用 camelCase(组件内)/kebab-case(模板)。
  • 用 Pinia 管理复杂、跨多层或需要持久化的状态。

9 速查表(一页纸版本)

  • 父→子:props / defineProps(只读)
  • 子→父:emit / defineEmits(事件)
  • 双向:v-model ⇄ prop modelValue + event update:modelValue(或使用 v-model:foo)
  • 父调子:ref + defineExpose()
  • 兄弟:提升状态(Parent 中转)或 Pinia / 事件总线(谨慎)
  • 跨层级:provide(reactive/ref) + inject
  • 全局:Pinia(推荐)
相关推荐
golang学习记2 小时前
从0死磕全栈之Next.js 中的 CSS 方案全解析:Global CSS、CSS Modules、Tailwind CSS 怎么选?
前端
Waker2 小时前
🚀 Turbo 使用指南
前端
立方世界3 小时前
CSS水平垂直居中方法深度分析
前端·css
恋猫de小郭3 小时前
Fluttercon EU 2025 :Let's go far with Flutter
android·前端·flutter
殇蓝3 小时前
react-lottie动画组件封装
前端·react.js·前端框架
05Nuyoah3 小时前
DAY 04 CSS文本,字体属性以及选择器
前端·css
一條狗3 小时前
学习日报 20250928|React 中实现 “实时检测”:useEffect 依赖项触发机制详解
前端·react.js
Gazer_S3 小时前
【React 状态管理深度解析:Object.is()、Hook 机制与 Vue 对比实践指南】
前端·react.js·前端框架