iT邦幫忙

2024 iThome 鐵人賽

DAY 29
1
Modern Web

欸你是要進 Vue 了沒?系列 第 29

欸你是要進 Vue 了沒? - Day29:Vue 組件間的溝通方式之 Emits、defineEmits() 子組件發出信號收到請回答 Over!

  • 分享至 

  • xImage
  •  

對,是系列文!
在 Vue 中,組件之間傳遞資料的機制有許多種,本系列主要介紹父、子組件之間的溝通,分別為:「父傳子:Props」&&「子傳父:Emits」。

這兩篇都是學習使用 <script setup> 的語法糖喔,官方文件另有提供非此語法的寫法,大家可以再爬爬。

提供一下今天的小摘要:

  • Emits 定義
  • Emits 沒有冒泡機制
    補充:什麼是狀態管理?
  • defineEmits() 語法
    陣列做為參數
    物件作為參數
    特性
    補充:編譯宏
  • 觸發事件
  • Emits 命名格式建議
  • 事件校驗

/images/emoticon/emoticon32.gif

我們~~開始嚕!

Emits 定義

Emits 是一種 從子組件傳遞事件給父組件 的機制。
這個機制透過子組件觸發事件的方式來表現,而父組件能夠接收子組件傳遞的事件。
https://ithelp.ithome.com.tw/upload/images/20241013/20169139p7z3v8mwHm.png

具體的操作如下:

  1. 子組件定義事件:可在子組件中使用 defineEmits() 語法來定義需要「拋出的事件」,讓子組件明確知道要觸發哪些自定義事件。
  2. 子組件觸發事件:當某個操作發生時,子組件使用 emit 方法來觸發定義的事件,將結果傳遞給父組件處理。
  3. 父組件監聽事件:父組件中須 import 子組件,並在使用子組件時,透過事件綁定(例如 @click-button="handler")來監聽子組件拋出的事件,並對事件做出處理。

Emits 沒有冒泡機制

和原生 DOM 事件不同,emits 觸發的事件不會冒泡。
這也代表:子組件觸發一個 emits 事件時,父組件「不會自動接收」到這個事件,所以必須明確地在父組件中進行監聽。

官方文件:而平級組件或是跨越多層嵌套的組件間通信,應使用一個外部的事件總線,或是使用一個全局狀態管理方案。

補充:什麼是狀態管理?

先引用一下官方文件的圖!

下面是“單向數據流”這一概念的簡單圖示:

https://ithelp.ithome.com.tw/upload/images/20241013/20169139wN0GBoE7qd.png

在 Vue 中,響應式狀態的運作流程,是以:「動作」>> 「狀態更新」>> 「介面更新」,這樣的循環來實現的,會遵循著「資料驅動介面」的原則,當狀態發生變化,Vue 會自動追蹤相關的依賴、更新相關的介面。

我們以計數器來理解這個「循環」是怎麼一回事:

<script setup>
import { ref } from "vue";

// 狀態
const count = ref(0);

// 動作
function increment() {
  count.value++;
}
</script>

<!-- 介面 -->
<template>
  <button @click="increment">{{ count }}</button>
</template>

這個計數器的運作像是這樣:
當使用者在介面觸發動作 >> 會更新狀態 >> 再更新介面。

而官方文件提到了:

然而,當我們有多個組件共享一個共同的狀態時,就沒有這麼簡單了:

  1. 多個視圖可能都依賴於同一份狀態。
  2. 來自不同視圖的交互也可能需要更改同一份狀態。

因此我們可以使用「狀態管理」這個思路來管理這些狀態,這個概念的核心在於:將狀態提升到全域,讓所有需要使用這些狀態的組件共享這些狀態。

常見的管理方式可以使用:reactive() 和 Pinia。

使用 reactive() 做簡單狀態管理

reactive() 可以做一些相對簡單的狀態管理。

當我們有兩個計數器組件,想要讓它們的狀態共享,可以這麼做:

  1. .js 檔案中用 reactive() 初始化狀態
// store.js
import { reactive } from "vue";

export const store = reactive({
  count: 0
})
  1. 於組件中 import 這個狀態做使用
// 組件 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>

我們看看瀏覽器上的呈現:
https://ithelp.ithome.com.tw/upload/images/20241013/20169139Xe1OOiBWVD.png
這兩個組件取得的 count 狀態,都來自 store.js 定義的 reactive({ count: 0 })

狀態共用的注意事項

而這邊需注意:由於狀態是共用的,當我們觸發某個「動作」時,如果沒有明確的分界,直接改動到了狀態本身,就會產生狀態的同步更新和連動效應!
(例如:在組件 A 設置點擊事件更改 count 的值,如果沒有進行合適的操作來區分動作範圍,有可能會改動到 組件 B count 的值)

而當我們在組件之間傳遞資料,並需要處理更複雜的狀態共享時,「狀態管理」對於提升程式碼的維護性,就是一個蠻好的思路~

而這邊就沒有帶大家進行更深入地講解了!
官方推薦可以使用 Pinia 進行更加靈活的狀態管理。
本菜也推薦大家看:

OK 我們繼續回到 Emits 囉,來看看究竟要怎麼使用語法來傳遞資料呢?

defineEmits() 語法

設置要「要傳遞的事件」。
可以用「這個事件名稱是一個發出信號的通道,會向父組件發送事件通知」來理解。

注意:事件的名稱必須為「字串」,因為在傳入監聽時,是以字串來傳遞的。

陣列做為參數

可以定義一個或多個事件。

<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() 語法有幾種特性可以注意:

  1. 不需要 import 就可以使用。
  2. 會 return 一個與 $emit 相同作用的函式。
  3. 所有傳入的參數都會被直接傳向父組件的監聽器。

馬上看一下子組件的使用範例:

<script setup>
// 1. 不需要 import 就可以使用。
const emit = defineEmits(["clickButton"]);
// 2. 會 return 一個與 $emit 相同作用的函式。
console.log(emit);
</script>
<template>
  <button @click="emit('clickButton')">我是一個按鈕,按我會傳遞訊息!</button> // 觸發事件
</template>

看一下在瀏覽器中 2. log 出來的樣子:
https://ithelp.ithome.com.tw/upload/images/20241013/20169139i1sSb142Ro.png
return 了一個函式!
這是 emitgetter 方法,當呼叫 emit(event, ...args) 時,會透過這個 getter 方法去呼叫組件實例的 emit 方法。
instance:是 Vue 3 中組件的實例的代稱。
https://ithelp.ithome.com.tw/upload/images/20241013/20169139TtvQ6Ioznt.png
(第 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>

Emits 命名格式建議

  1. 在子組件中,以 camelCase 作為命名方式。
<script setup>
const emit = defineEmits(['myEmitsEvent']);
</script>

<template>
  <button @click="emit('myEmitsEvent')">我是一個按鈕,按我會傳遞訊息!</button>
</template>
  1. 在父組件中,使用 kebab-case 命名方式監聽。
<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>

我們沿著剛剛提到的 具體操作要點 來檢視這個範例!

  1. 子組件定義事件:
    以下程式碼定義了事件,並且建立了校驗函式。
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 方法的函式,我們可以用它來觸發事件。
  1. 子組件觸發事件:
    當子組件 button@click 事件觸發時,會執行 send 表達式,
const send = function () {
  emit("sendNumber", inputNumber.value);
};

其中會執行 emit 函式,將「事件本身及 inputNumber.value 參數」發送至父組件。

  1. 父組件監聽事件:
  • 父組件中 import、並使用了子組件,因此父組件會成功接收到:子組件由 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 嚕。

還是要說 明天見鴨!
/images/emoticon/emoticon37.gif

範例 code ⬇️

https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day29

參考資料


上一篇
欸你是要進 Vue 了沒? - Day28:Vue 組件間的溝通方式之 Props、defineProps() 來自父組件の快遞請收下
下一篇
欸你是要進 Vue 了沒? - Day30:比賽過一半就想開始寫的完賽心得?
系列文
欸你是要進 Vue 了沒?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言