Vue 3 组件通信的 4 种正确姿势
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<p>上个月,我们重构一个老项目,发现一个“祖传组件”:</p>
<ul>
<li>父组件传 props 给子组件</li>
<li>子组件再传给孙子</li>
<li>孙子改了个状态,通过 <code>$emit</code> 一层层往上抛</li>
<li>中间任意一层改名,整条链就断了……</li>
</ul>
<p>同事苦笑:“这哪是组件通信,这是<strong>传话游戏</strong>。”</p>
<p>其实,Vue 3 早就提供了<strong>更优雅、更健壮</strong>的通信方案。<br>
今天我就用 <strong>4 种场景 + 对应解法</strong>,帮你彻底告别“props drilling”和“emit 地狱”。</p>
<hr>
<h2 data-id="heading-0">先看一张决策图(建议收藏)</h2>
</div>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202603/2149129-20260304144149396-436494512.png" alt="ScreenShot_2026-03-04_144140_872 - 副本" loading="lazy"></p>
<p> </p>
<blockquote>
<p>注意:不是所有通信都要用 Pinia! 小范围状态用轻量方案更干净。</p>
</blockquote>
<hr>
<h2 data-id="heading-1">姿势 1:父子通信 —— 老老实实用 props/emit(但要规范)</h2>
<p>这是最基础的,但很多人写得乱:</p>
<p>反面教材:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;"><!-- Child.vue -->
<script setup>
const emit = defineEmits(['update-name', 'save', 'cancel', 'validate']);
// 4 个 emit?这个组件到底负责什么?
</script></pre>
</div>
<p>正确做法:单一职责 + 语义化命名</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;"><!-- UserForm.vue -->
<script setup>
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ (e: 'update:modelValue', val: string): void }>();
const localValue = ref(props.modelValue);
watch(() => props.modelValue, v => localValue.value = v);
const handleChange = () => {
emit('update:modelValue', localValue.value); // 使用 v-model 语法糖
};
</script>
</pre>
</div>
<blockquote>
<p>技巧:用 <code>v-model</code> 代替自定义 <code>update-xxx</code>,模板更简洁:</p>
</blockquote>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;"><UserForm v-model="userName" /></pre>
</div>
<h2 data-id="heading-2">姿势 2:祖孙通信 —— 用 <code>provide / inject</code> 跳过中间层</h2>
<p>当你需要从 App.vue 直接传数据到深度嵌套的 Button 组件,别再层层传 props!</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// App.vue
import { provide, ref } from 'vue';
const theme = ref<'light' | 'dark'>('light');
provide('THEME', theme); // 提供响应式数据</pre>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;"><!-- DeepChildButton.vue -->
<script setup>
import { inject } from 'vue';
const theme = inject('THEME'); // 自动获得响应性!
</script>
<template>
<button :class="theme">Click me</button>
</template>
</pre>
</div>
<blockquote>
<p> 关键点:</p>
<ul>
<li>如果 <code>provide</code> 的是 <code>ref</code> 或 <code>reactive</code>,<code>inject</code> 拿到的就是响应式的</li>
<li>可以配合 TypeScript 定义 InjectionKey,避免字符串魔法值</li>
</ul>
</blockquote>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// types.ts
import { InjectionKey, Ref } from 'vue';
export const THEME_KEY: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme');</pre>
</div>
<div>
<div>
<h2 data-id="heading-3">姿势 3:任意组件通信 —— 用 Composable 封装共享状态(90% 的人不知道!)</h2>
<p>这是 Vue 3 最被低估的能力!</p>
<p>想象:<strong>两个不相关的弹窗,需要共享“是否正在提交”状态</strong>。</p>
<p>错误做法:把状态提到父组件,或滥用 Pinia</p>
<p>正确做法:写一个 <code>useSubmitState</code> composable:</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// composables/useSubmitState.ts
import { ref } from 'vue';
const isSubmitting = ref(false);
export function useSubmitState() {
const start = () => isSubmitting.value = true;
const end = () => isSubmitting.value = false;
return { isSubmitting, start, end };
}</pre>
</div>
<p>然后在任意组件中使用:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;"><!-- ModalA.vue -->
<script setup>
import { useSubmitState } from '@/composables/useSubmitState';
const { isSubmitting, start } = useSubmitState();
const handleSubmit = () => {
start();
// ...提交逻辑
};
</script></pre>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;"><!-- ModalB.vue -->
<script setup>
import { useSubmitState } from '@/composables/useSubmitState';
const { isSubmitting } = useSubmitState(); // 实时同步!
</script>
</pre>
</div>
<blockquote>
<p> 优势:</p>
<ul>
<li>零依赖(不用 Pinia)</li>
<li>天然响应式</li>
<li>可测试、可复用</li>
<li>作用域清晰(只在需要的组件引入)</li>
</ul>
</blockquote>
<h2 data-id="heading-4">姿势 4:全局状态 —— 交给 Pinia,别自己造轮子</h2>
<p>当状态涉及:</p>
<ul>
<li>用户登录信息</li>
<li>全局主题/语言</li>
<li>跨路由的数据缓存</li>
</ul>
<p>这时候就该用 Pinia(Vuex 的继任者,Vue 官方推荐):</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// stores/user.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', () => {
const profile = ref(null);
const isLoggedIn = computed(() => !!profile.value);
const login = async (credentials) => {
profile.value = await api.login(credentials);
};
return { profile, isLoggedIn, login };
});</pre>
</div>
<p>在组件中:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const userStore = useUserStore();
userStore.login({ email, password });
</pre>
</div>
<blockquote>
<p> Pinia 优势:</p>
<ul>
<li>Composition API 风格</li>
<li>完美 TS 支持</li>
<li>DevTools 调试友好</li>
<li>服务端渲染(SSR)兼容</li>
</ul>
</blockquote>
<h2 data-id="heading-5">总结:什么时候用哪种?</h2>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202603/2149129-20260304144526074-748074831.png" alt="ScreenShot_2026-03-04_144507_635" loading="lazy"></p>
<blockquote>
<p> 不要:</p>
<ul>
<li>用 <code>$parent</code> / <code>$children</code>(破坏封装)</li>
<li>用 EventBus(Vue 3 已废弃)</li>
<li>所有状态都塞进 Pinia(过度设计)</li>
</ul>
</blockquote>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19668094
頁:
[1]