Vue3 组件通信
【面试速答版】
Q1: “Vue3 中父组件如何向子组件传递数据?子组件如何触发父组件的事件?”
父传子用 props,子传父用 emit。在 Vue3 的 <script setup> 中,用 defineProps 声明 props,用 defineEmits 声明自定义事件,两者都不需要 import:
1 | <!-- 父 --> |
与 Vue2 的区别:Vue3 推荐 defineProps + defineEmits(在 script setup 中可用),替代了 Vue2 的 props 选项和 this.$emit。
Q2: “Vue3 的 v-model 有什么变化?如何实现多个双向绑定?”
Vue3 支持多个 v-model,每个绑定对应一对 prop + update:prop 事件。默认的 v-model 对应 modelValue prop 和 update:modelValue 事件。多个绑定写法:v-model:title="title" v-model:content="content"。Vue2 的 .sync 修饰符在 Vue3 中废弃,统一用多 v-model 替代。
Q3: “provide/inject 的原理是什么?适合什么场景?”
祖先组件通过 provide(key, value) 提供数据,后代组件通过 inject(key) 注入。适合跨多层组件传递且数据变化不频繁的场景,如主题(light/dark)、当前用户信息、语言设置。注意:默认不是响应式的——如果用 provide('count', ref(0)) 传一个 ref,后代才能感知变化。适合场景:深层传递、不需要额外引入状态管理库。
【深入理解版】
1. 这个知识点要解决什么问题?
组件化应用由父子组件嵌套构成,数据不可避免地需要跨越组件边界流动。主要场景:
- 父子之间:父组件传递配置给子组件,子组件反馈操作结果
- 祖孙之间:爷爷组件的数据,孙子组件需要用到,但中间两层”不关心”这个数据
- 兄弟之间:两个同级组件需要共享同一份状态(如购物车数据)
Vue3 提供了多种通信机制,不同场景选不同方式。选错了就会导致代码冗余或性能问题。
2. 核心原理/执行过程
2.1 props 的单向数据流
props 是 Vue 最基本的通信方式——数据从父组件流向子组件,子组件不能直接修改 props:
1 | <!-- 父组件 --> |
1 | <!-- 子组件 --> |
单项数据流:数据从父到子单向流动。如果子组件需要修改数据,通过 emit 事件通知父组件去改。这种模式的好处是数据变化路径清晰,便于追踪 bug。
2.2 emit 事件
子组件通过 defineEmits 声明事件,然后调用 emit 触发:
1 | <script setup> |
1 | <!-- 父组件 --> |
Vue3 vs Vue2 的 emit 差异:Vue2 中 $emit 直接挂载在组件实例上(this.$emit),Vue3 中 defineEmits 是编译时宏,在 <script setup> 中不需要 import。而且 defineEmits 可以做事件名校验——声明了的事件才能触发,没声明的事件触发时会在开发环境下警告。
2.3 多 v-model 替代 .sync
Vue3 的 v-model 本质是语法糖——v-model:title="title" 等价于:
1 | <Child :title="title" @update:title="val => title = val" /> |
所以 Vue3 支持多个 v-model 绑定:
1 | <Child |
子组件中:
1 | <script setup> |
Vue2 的 .sync 修饰符(:title.sync="title")做的事情完全一样,Vue3 统一为 v-model:propName,语法更一致。
2.4 provide/inject 的跨层级注入
当数据需要从爷爷传给孙子,中间隔了两层,用 props 就要经过两层不必要的传递:
1 | <!-- 爷爷组件 --> |
1 | <!-- 孙子组件(跳过中间的父/子层级) --> |
provide(key, value) 把数据注入到后代组件中。inject(key, defaultValue) 从祖先组件获取数据,如果找不到对应的 provide,使用默认值。
默认不是响应式的——如果你 provide('theme', 'dark') 传了一个字符串,后代拿到的是固定值,后续祖先修改这个值后代不会感知。需要传 ref 或 reactive 对象才能保持响应式。
3. 实际应用场景
场景1:props + emit 基础通信
1 | <!-- TodoList.vue --> |
1 | <!-- TodoItem.vue --> |
场景2:多 v-model 实现表单组件
1 | <!-- UserForm.vue(父组件) --> |
1 | <!-- FormInput.vue(子组件) --> |
4. 常见误区 & 实际项目中的坑
误区1:在子组件中直接修改 props
1 | <script setup> |
正确做法:emit 事件通知父组件修改。
误区2:认为 provide/inject 默认是响应式的
1 | // ❌ 传基础类型,后代感知不到变化 |
坑:v-model 默认 prop 名称变化
Vue2 的 v-model="count" 默认对应 value prop 和 input 事件。Vue3 改为 modelValue prop 和 update:modelValue 事件。迁移旧组件时要注意。
5. 与相关知识的关联 & 对比
| 通信方式 | 方向 | 适用场景 |
|---|---|---|
| props | 父→子 | 直接父子 |
| emit | 子→父 | 子组件反馈操作 |
| v-model | 父↔子 | 双向绑定 |
| provide/inject | 祖先→后代 | 跨层级、低频数据 |
| slots | 父→子(模板) | 定制渲染 |
| defineExpose / ref | 父→子 | 直接调用子组件方法 |
| 差异点 | Vue2 | Vue3 |
|---|---|---|
| 默认 v-model | value + input | modelValue + update:modelValue |
| .sync 修饰符 | 存在 | 废弃(多 v-model 替代) |
| EventBus | $on/$off(内置) | 移除(推荐 mitt) |
| $listeners | 独立对象 | 合并到 $attrs |
| defineExpose | 默认全部暴露 | 需显式声明 |
6. 现代最佳实践(2024-2025)
- 父子通信优先 props/emit + v-model,最显式、最符合单向数据流。
- 跨层级用 provide/inject 并传 ref/reactive 对象,保持响应式。
- 全局共享状态用 Pinia,不要自己用 provide/inject 实现全局状态管理——Pinia 帮你处理了状态隔离、响应式、DevTools 等。
- EventBus 场景用
mitt库(仅 200 字节),Vue3 已移除$on/$off。 - 使用 TypeScript 时用 defineProps 的类型声明形式:
1 | <script setup lang="ts"> |
7. 常见疑问解答
Q:provide/inject 和 Pinia 的适用边界在哪里?
A:provide/inject 适合跨 2-3 层但不会在多个无关页面之间共享的数据——如当前主题、当前用户信息。Pinia 适合跨页面、跨路由、多组件共享的全局状态——如购物车数据、用户设置。如果数据需要在完全不相关的两个页面之间共享,用 Pinia。如果数据只在”某个内部组件树”中传递,用 provide/inject。
Q:为什么 Vue3 要移除 $on/$off/$once?
A:$on/$off/$once 是 Vue2 实例上的事件发射器(EventBus)API。Vue 团队认为组件通信应该通过 props/emit、provide/inject、Pinia 等显式的机制,而不是”全局发布订阅”这种隐式方式。EventBus 在大型项目中会导致事件来源不明、命名冲突、调试困难的问题。如果确实需要跨组件的事件通信,用 mitt 库(200 字节)即可。
关联知识点索引
Vue3 核心变化与 Composition API.md— setup 中如何使用通信 APIPinia.md— 全局状态管理与 provide/inject 的适用边界