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
| export const SHARED_DATA_KEY = Symbol('sharedData');
|
provide/inject 的主要限制是可追蹤性,不是應用規模。依賴關係是隱含的,IDE 難以自動追蹤哪個元件注入了什麼,重構時容易漏改。如果資料流向複雜、需要 devtools 追蹤狀態變更,才需要考慮 Pinia。
3.2 全域狀態管理:Pinia
Pinia 是 Vue 官方現在推薦的狀態管理方案,API 比 Vuex 精簡,TypeScript 支援也更完整。
安裝
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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 |