Vue 组件通信方式有哪些?
简化版
Vue 组件通信常见方式有 props/emit、v-model、provide/inject、模板 ref 和状态管理 Pinia。父子通信优先 props/emit,跨层级可用 provide/inject,全局共享状态用 Pinia;事件总线只适合少量受控的解耦事件,不应作为默认状态通道。
详细版
父传子:
<Child :title="title" />
子传父:
const emit = defineEmits(['change']);
emit('change', value);
跨层级:
provide('theme', theme);
const theme = inject('theme');
全局状态:
const userStore = useUserStore();
面试回答要强调按关系选择,不要所有问题都上状态管理。状态越全局,维护成本越高。
完整版教学
一、父子通信是最基础的单向数据流
props 从父到子,emit 从子到父。这让数据来源清楚,变化路径也清楚。
子组件不应该直接修改 prop,而应该通过事件通知父组件修改。这样父组件仍然是状态所有者。
二、v-model 是语法糖
组件上的 v-model 本质上是 prop + emit:
<Child v-model="value" />
通常对应 modelValue 和 update:modelValue。Vue3 也支持多个 v-model。
三、provide/inject 适合跨层级依赖
多层组件传递同一个配置时,如果层层 props 很繁琐,可以用 provide/inject。常见场景有主题、表单上下文、组件库内部配置。
它不适合滥用成全局状态中心,否则数据来源会变隐蔽。
四、Pinia 适合全局共享状态
用户信息、权限、购物车、全局配置等跨页面共享状态适合放 Pinia。局部状态仍然应该留在组件内,避免 store 过度膨胀。
Pinia 解决的是多个不相邻消费者对同一业务状态的读取和修改,而不是免去状态建模。store 仍要划清业务边界,通过明确的 action 或方法表达修改意图,并避免把弹窗开关这类单组件临时状态永久留在全局。
五、面试追问与工程落地
组件通信常见追问是“兄弟组件怎么通信”。如果是近距离兄弟,通常把状态提升到共同父组件,通过 props 和 emit 分发;如果距离远或跨页面共享,再考虑 Pinia。不要一上来就事件总线或全局 store。
还会问 provide/inject 是否响应式。provide 传普通值就只是普通值;传 ref、reactive 或 computed,注入方才能拿到响应式连接。组件库里常用 provide/inject 传表单上下文、主题、注册方法。
工程里通信方式选择反映架构能力。局部状态越靠近使用者越好,全局状态要克制。很多维护灾难来自“为了方便”把临时状态放进全局 store,最后没人知道谁在改它。
六、按状态所有权和通信距离做选择
通信方案的核心不是“能不能传到”,而是谁拥有状态、谁有权修改。父组件持有状态时,子组件通过 prop 读取、通过 emit 表达意图,数据变化形成可追踪的闭环;跨越多层但仍属于同一组件子树的依赖适合 provide/inject;真正跨页面、跨业务模块共享的状态才进入 Pinia。
父组件 state
│ props(数据向下)
▼
子组件 UI
│ emit(意图向上)
└──────────────► 父组件更新 state
| 场景 | 首选方案 | 代价或边界 |
|---|---|---|
| 父子数据与事件 | props / emit | 层级很深时逐层透传繁琐 |
| 表单双向契约 | 组件 v-model | 本质仍是 prop + update 事件 |
| 祖先向后代提供上下文 | provide / inject | 来源不在模板上,调试要有约定 |
| 父组件调用子组件公开能力 | template ref + defineExpose | 耦合较强,只用于命令式能力 |
| 跨页面业务状态 | Pinia | 生命周期更长,需控制全局污染 |
| 父提供内容结构 | slot | 传递的是渲染内容,不是共享状态 |
假设状态只被 2 个兄弟组件使用,把它提升到 1 个共同父组件只形成 3 个明确连接;直接放进全局 store 则让整个应用都可能读写它,潜在修改者范围被无谓放大。状态应尽量靠近消费者,这是降低维护成本的直接手段。
provide 传 ref、reactive 或 computed 时可以保持响应式连接,修改逻辑最好也由提供方一起暴露,注入方不要随意修改共享 ref。字符串 key 在大型应用中可能冲突,库和插件常用 Symbol 作为注入 key。
const theme = ref('light')
provide(ThemeKey, {
theme: readonly(theme),
setTheme: (value: string) => { theme.value = value }
})
这个接口把“可读状态”和“修改动作”同时交给后代,但状态本身是只读的。这样既保留跨层响应式,也把写权限收回提供方,测试时可以分别验证读取契约和更新动作。
记忆钩子:先找状态所有者,再看通信距离;距离只是选工具的第二步,不能为了“少写两行”放弃清晰的数据所有权。
七、常见误区与追问
- 误区:组件通信越统一到 Pinia 越好。 局部状态全局化会延长生命周期、扩大修改范围,并让组件复用和测试更困难。
- 误区:provide/inject 天然会把普通值变成响应式。 响应式取决于提供的值;传 ref、reactive 或 computed 才保留相应连接。
- 误区:子组件可以直接修改对象 prop 的内部字段。 技术上深层修改可能成功,但会破坏单向数据流,通常应 emit 意图让所有者更新。
- 追问:兄弟组件怎样通信? 近距离时把状态提升到共同父组件,再用 props/emit;跨页面共享时再考虑 Pinia。
- 追问:组件 v-model 在 Vue3 中展开成什么? 默认对应
modelValueprop 与update:modelValue事件,也可以通过参数定义多个模型。 - 追问:什么时候可以使用模板 ref 调子组件? 聚焦、滚动、播放等命令式能力可以适度使用,普通数据同步不应绕过 props/emit。
- 追问:slot 属于组件通信吗? 它让父组件把渲染内容和作用域数据交给子组件布局,解决内容组合问题,不等同于共享可变状态。
八、加强记忆
组件通信按距离选方案:父子 props/emit,双向表单 v-model,跨层 provide/inject,全局共享 Pinia。不要为了方便把所有状态都塞进全局。
答题时再补一句“状态归谁、修改入口在哪”,就能从 API 罗列上升到可维护的数据流设计。