博客 / 詳情

返回

Vue 3 組件通信的 4 種正確姿勢

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

上個月,我們重構一個老項目,發現一個“祖傳組件”:

  • 父組件傳 props 給子組件
  • 子組件再傳給孫子
  • 孫子改了個狀態,通過 $emit 一層層往上拋
  • 中間任意一層改名,整條鏈就斷了……

同事苦笑:“這哪是組件通信,這是傳話遊戲。”

其實,Vue 3 早就提供了更優雅、更健壯的通信方案。
今天我就用 4 種場景 + 對應解法,幫你徹底告別“props drilling”和“emit 地獄”。


先看一張決策圖(建議收藏)

ScreenShot_2026-03-04_144140_872 - 副本

 

注意:不是所有通信都要用 Pinia! 小範圍狀態用輕量方案更乾淨。


姿勢 1:父子通信 —— 老老實實用 props/emit(但要規範)

這是最基礎的,但很多人寫得亂:

反面教材:

<!-- Child.vue -->
<script setup>
const emit = defineEmits(['update-name', 'save', 'cancel', 'validate']);
// 4 個 emit?這個組件到底負責什麼?
</script>

正確做法:單一職責 + 語義化命名

<!-- 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>

技巧:用 v-model 代替自定義 update-xxx,模板更簡潔:

<UserForm v-model="userName" />

姿勢 2:祖孫通信 —— 用 provide / inject 跳過中間層

當你需要從 App.vue 直接傳數據到深度嵌套的 Button 組件,別再層層傳 props!

// App.vue
import { provide, ref } from 'vue';

const theme = ref<'light' | 'dark'>('light');
provide('THEME', theme); // 提供響應式數據
<!-- DeepChildButton.vue -->
<script setup>
import { inject } from 'vue';

const theme = inject('THEME'); // 自動獲得響應性!
</script>

<template>
  <button :class="theme">Click me</button>
</template>

  關鍵點:

  • 如果 provide 的是 ref 或 reactiveinject 拿到的就是響應式的
  • 可以配合 TypeScript 定義 InjectionKey,避免字符串魔法值
// types.ts
import { InjectionKey, Ref } from 'vue';
export const THEME_KEY: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme');

姿勢 3:任意組件通信 —— 用 Composable 封裝共享狀態(90% 的人不知道!)

這是 Vue 3 最被低估的能力!

想象:兩個不相關的彈窗,需要共享“是否正在提交”狀態

錯誤做法:把狀態提到父組件,或濫用 Pinia

正確做法:寫一個 useSubmitState composable:

// 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 };
}

然後在任意組件中使用:

<!-- ModalA.vue -->
<script setup>
import { useSubmitState } from '@/composables/useSubmitState';
const { isSubmitting, start } = useSubmitState();

const handleSubmit = () => {
  start();
  // ...提交邏輯
};
</script>
<!-- ModalB.vue -->
<script setup>
import { useSubmitState } from '@/composables/useSubmitState';
const { isSubmitting } = useSubmitState(); // 實時同步!
</script>

  優勢:

  • 零依賴(不用 Pinia)
  • 天然響應式
  • 可測試、可複用
  • 作用域清晰(只在需要的組件引入)

姿勢 4:全局狀態 —— 交給 Pinia,別自己造輪子

當狀態涉及:

  • 用户登錄信息
  • 全局主題/語言
  • 跨路由的數據緩存

這時候就該用 Pinia(Vuex 的繼任者,Vue 官方推薦):

// 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 };
});

在組件中:

const userStore = useUserStore();
userStore.login({ email, password });

  Pinia 優勢:

  • Composition API 風格
  • 完美 TS 支持
  • DevTools 調試友好
  • 服務端渲染(SSR)兼容

總結:什麼時候用哪種?

ScreenShot_2026-03-04_144507_635

 不要:

  • 用 $parent / $children(破壞封裝)
  • 用 EventBus(Vue 3 已廢棄)
  • 所有狀態都塞進 Pinia(過度設計)

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.