Vue 3 組件化架構的核心問題之一,就是資料怎麼在元件之間流動。本文整理三個主要機制:父傳子用 props、子傳父用 emit、跨層級用 provide/inject,並附上 Vuex 與 Pinia 的對照。所有範例均採 <script setup> 語法。

1. 父元件傳資料到子元件(Props)

父元件透過屬性綁定把資料往下傳,子元件用 defineProps 宣告接收。

父元件

1
2
3
4
5
6
7
8
9
10
<template>
<ChildComponent :message="messageFromParent" />
</template>

<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

const messageFromParent = ref('Hello from Parent');
</script>

子元件

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>{{ message }}</div>
</template>

<script setup>
defineProps({
message: {
type: String,
required: true
}
});
</script>

defineProps 是編譯器巨集,不需要 import,在 <script setup> 中直接呼叫即可。宣告型別除了物件語法,也可以用 TypeScript 泛型寫法:

1
2
3
<script setup lang="ts">
const props = defineProps<{ message: string }>();
</script>

props 是單向的,子元件不應直接修改它。若需要可寫的本地值,用 ref 複製一份或改用 emit 通知父元件更新。

2. 子元件傳資料到父元件(Emit)

子元件用 defineEmits 宣告事件清單,再呼叫 emit 發出事件。

子元件

1
2
3
4
5
6
7
8
9
10
11
<template>
<button @click="sendToParent">Send to Parent</button>
</template>

<script setup>
const emit = defineEmits(['message-from-child']);

function sendToParent() {
emit('message-from-child', 'Hello from Child');
}
</script>

父元件

1
2
3
4
5
6
7
8
9
10
11
<template>
<ChildComponent @message-from-child="handleMessage" />
</template>

<script setup>
import ChildComponent from './ChildComponent.vue';

function handleMessage(msg) {
console.log('Received:', msg);
}
</script>

defineEmits<script setup> 中是必要的;若省略,TypeScript 會警告,且事件型別無法推導。

3. 跨層級資料傳遞

3.1 Provide / Inject

當資料需要跨越多個中間層傳到深層子元件,逐層 props 傳遞(prop drilling)會讓程式碼難以維護。這時可以用 provide/inject:祖先元件 provide 一份資料,任何後代元件都能 inject 取用,不論中間隔了幾層。

提供方(祖先元件)

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref, provide, readonly } from 'vue';

const sharedData = ref('這是要共享的資料');

// readonly 包裝讓下游無法直接改動來源
provide('sharedDataKey', {
sharedData: readonly(sharedData),
updateSharedData: (val) => { sharedData.value = val; }
});
</script>

接收方(任意後代元件)

1
2
3
4
5
6
7
8
9
10
<template>
<div>{{ sharedData }}</div>
<button @click="updateSharedData('新值')">更新</button>
</template>

<script setup>
import { inject } from 'vue';

const { sharedData, updateSharedData } = inject('sharedDataKey');
</script>

幾個注意點:

  • inject 的鍵名必須與 provide 完全一致。
  • 建議把修改邏輯封在 provide 端(傳出一個 function),讓狀態的讀寫保持在同一處,比較好追蹤。
  • 在大型專案裡,多個 provide 鍵名可能衝突;用 Symbol 當鍵可以完全避免這個問題。
1
2
// keys.js
export const SHARED_DATA_KEY = Symbol('sharedData');

provide/inject 的主要限制是可追蹤性,不是應用規模。依賴關係是隱含的,IDE 難以自動追蹤哪個元件注入了什麼,重構時容易漏改。如果資料流向複雜、需要 devtools 追蹤狀態變更,才需要考慮 Pinia。

3.2 全域狀態管理:Pinia

Pinia 是 Vue 官方現在推薦的狀態管理方案,API 比 Vuex 精簡,TypeScript 支援也更完整。

安裝

1
npm install pinia

main.js

1
2
3
4
5
6
7
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);
app.use(createPinia());
app.mount('#app');

定義 Store(stores/counterStore.js)

1
2
3
4
5
6
7
8
9
10
11
12
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++;
}
}
});

在元件中使用

1
2
3
4
5
6
7
8
9
10
<template>
<div>{{ counterStore.count }}</div>
<button @click="counterStore.increment">+1</button>
</template>

<script setup>
import { useCounterStore } from './stores/counterStore';

const counterStore = useCounterStore();
</script>

Pinia store 是響應式的,counterStore.count 可以直接在 template 裡綁定,不需要額外 computed。

3.3 Vuex 4(既有專案參考)

新專案建議直接用 Pinia。若你維護的是已用 Vuex 的 Vue 3 專案,Vuex 4 支援 Composition API 的 useStore()

1
npm install vuex  // 安裝到 4.x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store/index.js
import { createStore } from 'vuex';

export default createStore({
state: () => ({
sharedData: ''
}),
mutations: {
updateSharedData(state, payload) {
state.sharedData = payload;
}
},
getters: {
sharedData: state => state.sharedData
}
});

<script setup> 裡使用 Vuex

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';

const store = useStore();
const sharedData = computed(() => store.getters.sharedData);

function updateData() {
store.commit('updateSharedData', '新的共享資料');
}
</script>

useStore() 是在 <script setup> 中存取 Vuex store 的正確方式;this.$store 在 Composition API 中無法使用。

小結:怎麼選

場景 建議方案
父 → 子單向傳資料 props + defineProps
子 → 父回傳事件 emit + defineEmits
跨層級、依賴關係單純 provide / inject
多元件共享、需要 devtools 追蹤 Pinia
既有 Vuex 4 專案 useStore() + Composition API