對,是系列文!
在 Vue 中,組件之間傳遞資料的機制有許多種,本系列主要介紹父、子組件之間的溝通,分別為:「父傳子:Props」&&「子傳父:Emits」。
這兩篇都是學習使用 <script setup>
的語法糖喔,官方文件另有提供非此語法的寫法,大家可以再爬爬。
提供一下今天的小摘要:
- Emits 定義
- Emits 沒有冒泡機制
補充:什麼是狀態管理?- defineEmits() 語法
陣列做為參數
物件作為參數
特性
補充:編譯宏- 觸發事件
- Emits 命名格式建議
- 事件校驗
我們~~開始嚕!
Emits 是一種 從子組件傳遞事件給父組件 的機制。
這個機制透過子組件觸發事件的方式來表現,而父組件能夠接收子組件傳遞的事件。
具體的操作如下:
defineEmits()
語法來定義需要「拋出的事件」,讓子組件明確知道要觸發哪些自定義事件。emit
方法來觸發定義的事件,將結果傳遞給父組件處理。@click-button="handler"
)來監聽子組件拋出的事件,並對事件做出處理。和原生 DOM 事件不同,emits
觸發的事件不會冒泡。
這也代表:子組件觸發一個 emits
事件時,父組件「不會自動接收」到這個事件,所以必須明確地在父組件中進行監聽。
官方文件:而平級組件或是跨越多層嵌套的組件間通信,應使用一個外部的事件總線,或是使用一個全局狀態管理方案。
先引用一下官方文件的圖!
下面是“單向數據流”這一概念的簡單圖示:
在 Vue 中,響應式狀態的運作流程,是以:「動作」>> 「狀態更新」>> 「介面更新」,這樣的循環來實現的,會遵循著「資料驅動介面」的原則,當狀態發生變化,Vue 會自動追蹤相關的依賴、更新相關的介面。
我們以計數器來理解這個「循環」是怎麼一回事:
<script setup>
import { ref } from "vue";
// 狀態
const count = ref(0);
// 動作
function increment() {
count.value++;
}
</script>
<!-- 介面 -->
<template>
<button @click="increment">{{ count }}</button>
</template>
這個計數器的運作像是這樣:
當使用者在介面觸發動作 >> 會更新狀態 >> 再更新介面。
而官方文件提到了:
然而,當我們有多個組件共享一個共同的狀態時,就沒有這麼簡單了:
- 多個視圖可能都依賴於同一份狀態。
- 來自不同視圖的交互也可能需要更改同一份狀態。
因此我們可以使用「狀態管理」這個思路來管理這些狀態,這個概念的核心在於:將狀態提升到全域,讓所有需要使用這些狀態的組件共享這些狀態。
常見的管理方式可以使用:reactive()
和 Pinia。
reactive()
可以做一些相對簡單的狀態管理。
當我們有兩個計數器組件,想要讓它們的狀態共享,可以這麼做:
.js
檔案中用 reactive()
初始化狀態// store.js
import { reactive } from "vue";
export const store = reactive({
count: 0
})
// 組件 A
<script setup>
import { store } from "./store";
</script>
<template>
<div>
計數器 A:{{ store.count }}
</div>
</template>
// 組件 B
<script setup>
import { store } from "./store";
</script>
<template>
<div>
計數器 B:{{ store.count }}
</div>
</template>
我們看看瀏覽器上的呈現:
這兩個組件取得的 count
狀態,都來自 store.js
定義的 reactive({ count: 0 })
。
而這邊需注意:由於狀態是共用的,當我們觸發某個「動作」時,如果沒有明確的分界,直接改動到了狀態本身,就會產生狀態的同步更新和連動效應!
(例如:在組件 A 設置點擊事件更改 count
的值,如果沒有進行合適的操作來區分動作範圍,有可能會改動到 組件 B count
的值)
而當我們在組件之間傳遞資料,並需要處理更複雜的狀態共享時,「狀態管理」對於提升程式碼的維護性,就是一個蠻好的思路~
而這邊就沒有帶大家進行更深入地講解了!
官方推薦可以使用 Pinia 進行更加靈活的狀態管理。
本菜也推薦大家看:
OK 我們繼續回到 Emits 囉,來看看究竟要怎麼使用語法來傳遞資料呢?
設置要「要傳遞的事件」。
可以用「這個事件名稱是一個發出信號的通道,會向父組件發送事件通知」來理解。
注意:事件的名稱必須為「字串」,因為在傳入監聽時,是以字串來傳遞的。
可以定義一個或多個事件。
<script setup>
defineEmits(["你定義的 emits 事件名稱"]);
</script>
// 定義多個事件
<script setup>
defineEmits(["你定義的 emits 事件名稱","另一個你定義的 emits 事件名稱"]);
</script>
可添加校驗函式。
const emit = defineEmits({
你定義的 emits 事件名稱: () => {
// ... 校驗事件邏輯
},
另一個你定義的 emits 事件名稱: () => {
// ... 校驗事件邏輯
}
})
看看實際範例寫法:
const emit = defineEmits({
// 沒有校驗
click: null,
// 校驗 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
defineEmits()
語法有幾種特性可以注意:
$emit
相同作用的函式。馬上看一下子組件的使用範例:
<script setup>
// 1. 不需要 import 就可以使用。
const emit = defineEmits(["clickButton"]);
// 2. 會 return 一個與 $emit 相同作用的函式。
console.log(emit);
</script>
<template>
<button @click="emit('clickButton')">我是一個按鈕,按我會傳遞訊息!</button> // 觸發事件
</template>
看一下在瀏覽器中 2. log
出來的樣子:
return 了一個函式!
這是 emit
的 getter
方法,當呼叫 emit(event, ...args)
時,會透過這個 getter 方法去呼叫組件實例的 emit
方法。instance
:是 Vue 3 中組件的實例的代稱。
(第 3 點,多個參數的舉例我們稍後會再來看~)
defineEmits()
是一種只能在 <script setup>
中使用的編譯宏,編譯宏是指在編譯器編譯時,會將傳入的參數轉為文字替換值,而不會對其做表達式的求值或類型檢查的操作。
在這邊就是將 ()
中的值做了文本替換,暴露到模板,是一種預處理的操作~
使用了 defineEmits()
定義事件,它會 return 一個與 $emit
相同作用的函式。
而要將事件信號發送給父組件,必須觸發這個函式。
我們可以將 return 的函式賦值給一個變數,並使用變數來觸發事件:
// 以 emit 變數來接
const emit = defineEmits(["你定義的 emits 事件名稱"]);
(通常這個變數會命名為 emit,以下以 emit 變數來做觸發~)
// 觸發事件
emit('你定義的 emits 事件名稱');
// 使用表達式作為參數來觸發事件
emit('事件觸發時要執行的表達式');
其中可以帶 自定義的參數 於事件後方(參數可以為多個):
// 觸發事件
emit('你定義的 emits 事件名稱','要傳入父組件的參數');
// 使用表達式作為參數來觸發事件
emit('事件觸發時要執行的表達式','要傳入父組件的參數');
來看看以 @click
事件觸發的範例:
<script setup>
const emit = defineEmits(['clickButton']);
</script>
<template>
<button @click="emit('clickButton');">觸發事件</button>
</template>
// 觸發時傳入自定義參數
<script setup>
const emit = defineEmits(['clickButton']);
</script>
<template>
<button @click="emit('clickButton','哈嚕');">觸發事件</button>
</template>
// 表達式作為參數的觸發
<script setup>
const emit = defineEmits(["clickButton"]);
function handleClick() {
emit('clickButton');
}
</script>
<template>
<button @click="handleClick">觸發事件</button>
</template>
<script setup>
const emit = defineEmits(['myEmitsEvent']);
</script>
<template>
<button @click="emit('myEmitsEvent')">我是一個按鈕,按我會傳遞訊息!</button>
</template>
<script setup>
const handler = () => {
console.log('事件已被觸發!');
};
</script>
<template>
<MyComponent @my-emits-event="handler" />
</template>
官方文件:和對 props 添加類型校驗的方式類似,所有觸發的事件也可以使用對象形式來描述。
要為事件添加校驗,那麼事件可以被賦值為一個函數,接受的參數就是拋出事件時傳入 emit 的內容,返回一個布爾值來表明事件是否合法。
要於事件觸發時進行「驗證」,可以在使用 defineEmits()
定義事件時,傳入「校驗函式及其參數」,並於使用條件判斷來檢查參數的有效性,根據結果執行相應的邏輯。例如:在驗證失敗時,可以 return false
,瀏覽器會自動觸發事件校驗失敗的警告。
但注意:事件還是會被傳遞到父組件哦!
不過編寫校驗,可以讓自己或其他開發者意識到可能出現的錯誤,作出相應處理。
我們以例子直接看~
// 子組件
<script setup>
import { ref } from "vue";
const inputNumber = ref("");
const emit = defineEmits({
sendNumber: (value) => {
const numberValue = Number(value);
if (!isNaN(numberValue)) {
return true; // 校驗成功
} else {
console.log("請輸入有效的數字!!!");
return false; // 校驗失敗
}
},
});
const send = function () {
emit("sendNumber", inputNumber.value); // 發送事件
};
</script>
<template>
<p>
<label for="inputNumber">
請輸入一個數字:
<input id="inputNumber" v-model="inputNumber" />
</label>
</p>
<button type="button" @click="send">幫我傳過去</button>
</template>
// 父組件
<script setup>
import ChildEmit3 from "./childEmit3.vue";
const handler = function (data) {
console.log(`子組件傳來的資料:`, data);
};
</script>
<template>
<ChildEmit3 @send-number="handler" />
</template>
我們沿著剛剛提到的 具體操作要點 來檢視這個範例!
const emit = defineEmits({
sendNumber: (value) => {
const numberValue = Number(value);
if (!isNaN(numberValue)) {
return true;
} else {
console.log("請輸入有效的數字!!!");
return false;
}
},
});
defineEmits();
參數設置為物件,其中傳入 sendNumber: (value) => {}
箭頭函式,箭頭函式會執行:將傳入的 value
轉為數字,並且根據邏輯進入條件判斷。emit
變數,這個變數會 return 一個同等 $emit
方法的函式,我們可以用它來觸發事件。button
的 @click
事件觸發時,會執行 send
表達式,const send = function () {
emit("sendNumber", inputNumber.value);
};
其中會執行 emit
函式,將「事件本身及 inputNumber.value
參數」發送至父組件。
emit()
函式傳過來的參數 "sendNumber", inputNumber.value
。<ChildEmit3 @send-number="handler" />
的 @send-number
v-on
語法監聽了子組件的 sendNumber
事件(在父組件需以 kebab-case 的命名方式),在事件觸發時,執行 handler
表達式,const handler = function (data) {
console.log(`子組件傳來的資料:`, data);
};
因此會印出:子組件傳遞過來的 inputNumber.value
參數。
我們來瀏覽器操作玩玩看:
這邊要注意,傳遞的數據未通過校驗,return false
時,會觸發事件校驗失敗的警告,不過事件依舊有傳到父組件哦。
收到惹,Over!
這兩篇學習父、子組件是如何傳遞資料的,發現只要靜下來想一下步驟的要點,定義清楚,其實就可以成功做到哩!
然後好像真的有微微寫 Vue 的感覺了誒⋯⋯例如已經可以自己刻好 v-for
,例如意識到 input
要用 v-model
就先寫,如果想要改響應式狀態就用 computed
,雖然常常忘記是在幹嘛又回去翻~(有記錄真的是會形成一個善的循環 XD)
而~組件間的溝通方式兩篇就在這邊跟搭家說 Bye Bye 嚕。
還是要說 明天見鴨!
https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day29