Vue父子组件通信有哪些实现方式?
Vue 中父子组件通信是开发中最基础也是最高频的场景。根据 Vue 2 和 Vue 3 的不同特性,以及通信的方向(父传子、子传父、双向),主要有以下 8 种 常见的实现方式:
1. Props(父传子)—— 最通用
这是最基础的方式。父组件通过属性绑定传递数据,子组件通过 props 接收。
- 适用场景: 父组件向子组件传递静态或动态数据。
- 特点: 单向数据流(父组件数据更新会流向子组件,反之不行)。
html
<!-- 父组件 -->
<Child :msg="message" />
<!-- 子组件 -->
<script>
props: {
msg: String
}
</script>
2. $emit / v-on(子传父)—— 最通用
子组件通过触发事件通知父组件,父组件监听该事件并接收数据。
- 适用场景: 子组件需要修改父组件数据,或通知父组件执行操作。
js
// 子组件
this.$emit('update-data', newValue);
// 父组件
<Child @update-data="handleUpdate" />
3. v-model(双向绑定)
v-model 本质上是 props + event 的语法糖。
- Vue 2: 默认为
valueprop 和input事件。 - Vue 3: 默认为
modelValueprop 和update:modelValue事件。支持多个 v-model。
html
<!-- 父组件 -->
<Child v-model="count" />
<!-- 子组件 (Vue 3 写法) -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<button @click="$emit('update:modelValue', modelValue + 1)">+1</button>
</template>
4. Refs & defineExpose(父访问子)
父组件通过 ref 获取子组件的实例,直接调用子组件的方法或访问数据。
- 注意:
- Vue 2: 直接访问
this.$refs.childName。 - Vue 3 (
<script setup>): 组件默认是关闭的(私有的),必须在子组件中通过defineExpose暴露属性或方法,父组件才能访问。
- Vue 2: 直接访问
js
// 子组件 (Vue 3)
const openModal = () => { ... }
defineExpose({ openModal }) // 显式暴露
// 父组件
const childRef = ref(null)
childRef.value.openModal()
5. Provide / Inject(依赖注入)
用于祖孙组件或层级很深的父子组件通信,避免“Props Drilling”(属性透传)。
- 适用场景: 通用的配置、主题、用户信息的传递。
- 特点: 父组件(Provider)提供数据,后代组件(Injector)无论多深都可以注入使用。若传递的是响应式对象(如
ref或reactive),则后代也能响应更新。
js
// 父组件 (Ancestor)
provide('theme', 'dark')
// 后代组件 (Descendant)
const theme = inject('theme')
6. $attrs(透传 Attributes)
当父组件传递的属性没有被子组件声明为 props 或 emits 时,这些属性会包含在 $attrs 中。
- 适用场景: 封装高阶组件(HOC)或基础 UI 组件时,将父组件传递的
class、style或原生事件直接透传给子组件内部的根元素或特定元素。 - Vue 3 变化:
$listeners已被移除,事件监听器现在也包含在$attrs中。
html
<!-- 父组件 -->
<BaseInput placeholder="请输入" @change="handleChange" />
<!-- 子组件 BaseInput -->
<template>
<!-- v-bind="$attrs" 将 placeholder 和 change 事件都绑定到了 input 上 -->
<input v-bind="$attrs" />
</template>
7. 作用域插槽 (Scoped Slots)
虽然主要用于内容分发,但它允许子组件将数据传递回父组件的插槽内容中。
- 适用场景: 父组件定义样式/布局,但数据内容由子组件提供(例如列表渲染组件)。
html
<!-- 子组件 -->
<slot :item="itemData"></slot>
<!-- 父组件 -->
<Child>
<template v-slot="{ item }">
<span>{{ item.name }}</span>
</template>
</Child>
8. children (不推荐)
- $parent: 子组件访问父组件实例。
- $children: 父组件访问子组件实例数组。
- 现状:
- Vue 2 中可用。
- Vue 3 中
$children已被移除。 - 弊端: 导致父子组件强耦合,难以维护,通常不建议在业务代码中使用。
总结与最佳实践
| 方式 | 方向 | 推荐指数 | 适用场景 |
|---|---|---|---|
| Props / Emit | 父 ↔ 子 | ⭐⭐⭐⭐⭐ | 最基础、最常用的业务逻辑通信。 |
| v-model | 双向 | ⭐⭐⭐⭐⭐ | 表单组件、状态同步组件。 |
| Provide / Inject | 跨层级 | ⭐⭐⭐⭐ | 插件开发、深层嵌套组件共享数据。 |
| Refs | 父 → 子 | ⭐⭐⭐ | 需要手动触发子组件方法(如聚焦、重置、打开弹窗)。 |
| Slots | 子 → 父(UI) | ⭐⭐⭐⭐ | 布局复用,逻辑与视图分离。 |
| Pinia / Vuex | 全局 | ⭐⭐⭐⭐⭐ | 兄弟组件通信、跨页面复杂状态管理。 |
| EventBus (mitt) | 任意 | ⭐⭐ | 简单项目中的非父子通信(Vue 3 已移除原生 off,需引入库)。 |
面试回答策略:
建议先说最常用的 Props 和 $emit,然后补充 v-model 和 Refs,最后提到跨层级的 Provide/Inject 和全局状态管理 Pinia,这样能体现你对 Vue 生态的全面掌握。