春歌拆除队电镐水钻 發表於 2026-3-4 14:46:00

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>&nbsp;</p>
<blockquote>
<p>注意:不是所有通信都要用 Pinia!&nbsp;小范围状态用轻量方案更干净。</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;">&lt;!-- Child.vue --&gt;
&lt;script setup&gt;
const emit = defineEmits(['update-name', 'save', 'cancel', 'validate']);
// 4 个 emit?这个组件到底负责什么?
&lt;/script&gt;</pre>
</div>
<p>正确做法:单一职责 + 语义化命名</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- UserForm.vue --&gt;
&lt;script setup&gt;
const props = defineProps&lt;{ modelValue: string }&gt;();
const emit = defineEmits&lt;{ (e: 'update:modelValue', val: string): void }&gt;();

const localValue = ref(props.modelValue);
watch(() =&gt; props.modelValue, v =&gt; localValue.value = v);

const handleChange = () =&gt; {
emit('update:modelValue', localValue.value); // 使用 v-model 语法糖
};
&lt;/script&gt;
</pre>
</div>
<blockquote>
<p>技巧:用&nbsp;<code>v-model</code>&nbsp;代替自定义&nbsp;<code>update-xxx</code>,模板更简洁:</p>
</blockquote>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;UserForm v-model="userName" /&gt;</pre>
</div>
<h2 data-id="heading-2">姿势 2:祖孙通信 —— 用&nbsp;<code>provide / inject</code>&nbsp;跳过中间层</h2>
<p>当你需要从&nbsp;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&lt;'light' | 'dark'&gt;('light');
provide('THEME', theme); // 提供响应式数据</pre>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- DeepChildButton.vue --&gt;
&lt;script setup&gt;
import { inject } from 'vue';

const theme = inject('THEME'); // 自动获得响应性!
&lt;/script&gt;

&lt;template&gt;
&lt;button :class="theme"&gt;Click me&lt;/button&gt;
&lt;/template&gt;
</pre>
</div>
<blockquote>
<p>  关键点:</p>
<ul>
<li>如果&nbsp;<code>provide</code>&nbsp;的是&nbsp;<code>ref</code>&nbsp;或&nbsp;<code>reactive</code>,<code>inject</code>&nbsp;拿到的就是响应式的</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&lt;Ref&lt;'light' | 'dark'&gt;&gt; = 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 = () =&gt; isSubmitting.value = true;
const end = () =&gt; isSubmitting.value = false;

return { isSubmitting, start, end };
}</pre>
</div>
<p>然后在任意组件中使用:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- ModalA.vue --&gt;
&lt;script setup&gt;
import { useSubmitState } from '@/composables/useSubmitState';
const { isSubmitting, start } = useSubmitState();

const handleSubmit = () =&gt; {
start();
// ...提交逻辑
};
&lt;/script&gt;</pre>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- ModalB.vue --&gt;
&lt;script setup&gt;
import { useSubmitState } from '@/composables/useSubmitState';
const { isSubmitting } = useSubmitState(); // 实时同步!
&lt;/script&gt;
</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>这时候就该用&nbsp;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', () =&gt; {
const profile = ref(null);
const isLoggedIn = computed(() =&gt; !!profile.value);

const login = async (credentials) =&gt; {
    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>&nbsp;不要:</p>
<ul>
<li>用&nbsp;<code>$parent</code>&nbsp;/&nbsp;<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]
查看完整版本: Vue 3 组件通信的 4 种正确姿势