iT邦幫忙

2024 iThome 鐵人賽

DAY 24
1
Modern Web

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

欸你是要進 Vue 了沒? - Day24:Vue 生命週期之從組件來到了這世界到它完成任務後離開的過程

  • 分享至 

  • xImage
  •  

蛤肉,大家好。
如標題所述,今天我們要來看 Vue 中一個重要的觀念:「生命週期」。

我們將會分為以下幾部分來學習:

  • 生命週期定義
  • 為什麼需要了解?
  • 初步概念
  • 階段流程
  • 組合式 API 鉤子函式
  • 組件間的生命週期互動與觸發順序

OK 我們開始!(沒在要跟你喘一下)
/images/emoticon/emoticon39.gif

定義

生命週期(Lifecycle Hooks),可以理解為:組件從出生到死亡的完整過程。

從一個組件的實例被創建、初始化,到呈現在介面上,再經歷資料變動和更新,最後,當它不再被需要時,就會被銷毀。

為什麼需要了解?

這段過程中的每個階段,Vue 都會「執行對應的操作」,而理解各階段的行為,可以幫助我們在適當的時機點介入,實現我們想要做的事情。

初步概念

在 Vue 的生命週期中,有這幾個階段可以注意:

  • Create 創建階段
  • Mount 掛載階段
  • Update 更新階段
  • Unmount 卸載階段

每個階段的前、後都有相應的鉤子函式(hook functions),讓我們可以在這些階段前後取得資料或執行特定行為。

官方提供的圖很清楚~跟著步驟一步一步看看組件們在不同階段都會做些什麼吧!

lifecycle_zh-CN.W0MNXI0C
圖片來源:官方文件

小道消息

圖中紅色方框的階段為「鉤子函式(hook functions)」,可以理解為在不同生命週期階段可執行的 API 操作。

Vue 3 的組合式 API 沒有 Create 階段的對應鉤子函式,而是將初始化的邏輯集中到了 setup() 函式中執行,和 Vue 2 的選項式 API 不同,具體差異可以參考以下:

流程開始!

什麼時候會觸發生命週期?

也就是流程圖的第一步:「渲染器遇到組件」。
只要遇到一個組件就會觸發。

Create 創建階段

也就是「渲染器遇到組件後~準備開始渲染之前」這個階段。
所有組件的數據都在這階段中「被初始化」,這確保了在渲染介面前準備好所有必要的資料。

image

setup(組合式 API)

生命週期的第一步是:執行組合式 API 中定義的 <script setup> 區塊,組合式 API 的實例會在這階段被創建。

初始化選項式 API

選項式 API 的實例會在這階段被創建。

其中又區分為:

  • beforeCreate(創建前)
  • created(創建後)

是否存在預編譯模板

接下來進入讀取組件中的 <template>,看組件中是否有 <template> 區塊。
image

  • 無 >> 即時編譯模板
    也就是檢查是否有需要動態操作的 <template>,可能是用程式碼定義的操作(渲染函式寫的 HTML 結構、JSX、手動操作的 DOM 語法等等)。
  • 有 >> 準備進入掛載階段,接著進行初始渲染、創建和插入 DOM 節點。

Mount 掛載階段

也就是渲染介面的時間點。

image

初始渲染,創建和插入 DOM 節點

其中又區分為:

  • beforeMount(掛載前):組件已完成響應式狀態的設置、尚未創建 DOM 節點。
  • mounted(掛載後):組件已完成創建 DOM 節點。

在完成第一次的渲染後,組件也就完成了初始掛載。
接著,組件將「保持在掛載階段中」(如下圖中的紅色圓圈的位置),直到銷毀前。
image

Update 更新階段

image

組件存在掛載階段中時,會有一個持續循環的過程,稱為更新階段(虛線圓圈的循環)。
這個過程中,因為資料變動,Vue 會重新渲染並打補丁(更新介面並 patch DOM 樹的過程),組件會一直循環這個過程,直到需要取消掛載。

Unmount 取消掛載(卸載)階段

當組件不再被需要時,就會進入卸載階段,準備被銷毀。
image

其中又區分為:

  • beforeUnmount(卸載前)
  • unmounted(卸載後)

以上是主要的階段流程,接下來來看看 Vue 提供給組合式 API 使用的鉤子函式吧~

組合式 API 鉤子函式

定義

鉤子函式的參數是一個 callback function,在定義時進行註冊,並會在對應的生命週期階段自動 callback 執行。

今天會帶大家看這幾種:

  • onBeforeMount()
  • onMounted()
  • onBeforeUpdate()
  • onUpdated()
  • onBeforeUnmount()
  • onUnmounted()

會介紹它們個別

  • 適用的呼叫時機
  • 組件在該階段中的狀態
  • 需要注意的部分

並會以範例帶大家比較使用時機~

注意:這幾種皆無法在服務器渲染 SSR 使用。
(在服務器渲染,伺服器主要負責生成靜態 HTML,再交到客戶端執行 DOM 操作,掛載階段是發生在客戶端,服務器端無法取得 DOM 元素,因此也無法執行完成掛載、更新、卸載相關的邏輯。)

onBeforeMount()、onMounted()

分界點為掛載階段:

  • onBeforeMount()
    呼叫時機:掛載前。
    組件狀態:已完成響應式狀態的設置、尚未創建 DOM 節點。
    注意:不能在這邊操作 DOM。
  • onMounted()
    呼叫時機:掛載後。
    組件狀態:組件已完成創建 DOM 節點,並插入到父組件,完成渲染。
    而所有同步的子組件都被掛載完成(不包含非同步及 樹內的組件)。
    注意:可以在這邊操作 DOM。

官方文件:注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。

補充:因為根組件是掛載的起點,其他 import 的子組件都將從這邊渲染,並建立 DOM,如果根組件沒有正確在檔案中,過程就無法開始。

兩者使用時機比較

掛載時機點,等同於 DOM 被渲染出來的時機點,因此我們於這兩個鉤子函式中操作 DOM 試試看。

使用 onMounted()

<script setup>
import { onMounted, onBeforeMount } from "vue";

onMounted(() => {
  console.log(document.getElementById("button").textContent);
});
</script>
<template>
  <button id="button">Mount</button>
</template>

正確拿到 button 中的文字。
onmounted

使用 onBeforeMount()

<script setup>
import { onMounted, onBeforeMount } from "vue";

onBeforeMount(() => {
  console.log(document.getElementById("button").textContent);
});
</script>
<template>
  <button id="button">Mount</button>
</template>

報錯了,因為頁面還沒有創建 DOM。

onUnmout

onBeforeUpdate()、onUpdated()

分界點為更新階段:

  • onBeforeUpdate()
    呼叫時機:更新資料前。
    組件狀態:準備變更響應式狀態、更新 DOM 樹。
    注意:在此階段更改資料是安全的。
  • onUpdated()
    呼叫時機:更新資料後。
    組件狀態:自身及所有子組件的響應式狀態及 DOM 樹都已完成更新。
    注意:在這階段更新數據,可能會造成無窮循環(更新後再次觸發更新,更新完了又更新⋯⋯)。

補充:Vue 的渲染機制,可在同一個組件更新不同的數據和 DOM,但只觸發一次 onUpdated()
所以官方建議:如果想在「特定」DOM 更新後執行某些操作,請使用 nextTick()

兩者使用時機比較

我們設置一個計數器使用的情境來看看~

  1. 綁定計數器響應式狀態 count
  2. 點擊按鈕觸發計數器資料更新。
  3. 將更新後的資料,同步介面。

我們分別在更新前、後,使用 console.log(document.getElementById("count").textContent); 來取得計數器文字。

使用 onUpdated()

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

const count = ref(0);

onUpdated(() => {
  console.log(document.getElementById("count").textContent);
});
</script>

<template>
  <button id="count" @click="count++">{{ count }}</button>
</template>

在這個階段,響應式狀態及 DOM 樹都已完成更新,因此能夠正確取得更新後的數字。

使用 onBeforeUpdate()

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

const count = ref(0);

onBeforeUpdate(() => {
  console.log(document.getElementById("count").textContent);
});
</script>

<template>
  <button id="count" @click="count++">{{ count }}</button>
</template>

在這個階段,響應式狀態及 DOM 樹還沒有更新,因此我們得到的是舊的值。

onBeforeUnmount()、onUnmounted()

分界點為卸載階段:

  • onBeforeUnmount()
    呼叫時機:卸載組件前。
    組件狀態:準備卸載,實例還保有全部的功能。
  • onUnmounted()
    呼叫時機:卸載組件後。
    組件狀態:自身及所有子組建皆已卸載完畢,實例已經被銷毀、毫無功能,所有相關的響應式渲染、setup() 創建的 computed、偵聽器都已被停止。

兩者使用時機比較

在一個父組件 unmount.vue 中使用 v-if 控制子組件 unmountChild.vue 的呈現。
定義鉤子函式,印出 document.getElementById("unmountChild").textContent 的文字 unmountChild

// unmount.vue
<script setup>
import { ref } from "vue";
import UnmountChild from "./unmountChild.vue";

const showChild = ref(true);
</script>
<template>
  <button @click="showChild = !showChild">切換卸載</button>
  <UnmountChild v-if="showChild" />
</template>
// unmountChild.vue
<script setup>
import { onUnmounted, onBeforeUnmount } from "vue";

onUnmounted(() => {
  console.log(
    `onUnmounted`,
    document.getElementById("unmountChild").textContent
  );
});
onBeforeUnmount(() => {
  console.log(
    `onBeforeUnmount`,
    document.getElementById("unmountChild").textContent
  );
});
</script>
<template>
  <div id="unmountChild">unmountChild</div>
</template>

使用 onBeforeUnmount()
點選了按鈕後,子組件會進入卸載階段,卸載前觸發了 onBeforeUmount() 函式,成功印出文字 unmountChild

使用 onUnmounted()
組件已經被卸載,取不到元素的 DOM,會報錯。

組件間的生命週期互動與觸發順序

組件之間的相互關聯通常會觸發多個生命週期,例如:當一個組件 import 另一個子組件時,會執行子組件的生命週期。

我們接下來會用這個結構去講解~

分別有三個組件:
componentA.vue
componentB.vue
componentC.vue

  1. componentA import componentB,
    componentB import componentC。
  2. componentB 有一個計數器,會更新數據。
  3. componentA 有按鈕綁定 v-if 控制 子組件 componentB 是否呈現。
  4. componentB 有按鈕綁定 v-if 控制 子組件 componentC 是否呈現。

接下來我們會分別觀察 Mount、Update、Unmount 這三個階段~

上程式碼:

// componentA.vue

<script setup>
import ComponentB from "./componentB.vue";
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, ref } from "vue";

onBeforeMount(() => {
  console.log(`componentA: before mount!`);
});
onMounted(() => {
  console.log(`componentA: mounted!`);
});
onBeforeUpdate(() => {
  console.log(`componentA: before update!`);
});
onUpdated(() => {
  console.log(`componentA: updated!`);
});
const showComponentB = ref(true);
</script>
<template>
  <div style="background-color: lavender; padding: 10px">
    <div style="font-size: 30px">componentA</div>
    <button @click="showComponentB = !showComponentB">
      componentB 切換顯示
    </button>
    <ComponentB v-if="showComponentB" />
  </div>
</template>
// componentB.vue

<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  ref,
} from "vue";
import ComponentC from "./componentC.vue";

onBeforeMount(() => {
  console.log(`componentB: before mount!`);
});
onMounted(() => {
  console.log(`componentB: mounted!`);
});
onBeforeUpdate(() => {
  console.log(`componentB: before update!`);
});
onUpdated(() => {
  console.log(`componentB: updated!`);
});
onBeforeUnmount(() => {
  console.log(`componentB: before unmount!`);
});
onUnmounted(() => {
  console.log(`componentB: unmounted!`);
});

const count = ref(0);
const showComponentC = ref(true);
</script>

<template>
  <div style="background-color: lightgrey; padding: 10px">
    <div style="font-size: 30px">componentB</div>
    <button @click="count++">計數器:{{ count }}</button>
    <button @click="showComponentC = !showComponentC">
      componentC 切換顯示
    </button>
    <ComponentC v-if="showComponentC" />
  </div>
</template>
// componentC.vue

<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from "vue";
onBeforeMount(() => {
  console.log(`componentC: before mount!`);
});
onMounted(() => {
  console.log(`componentC: mounted!`);
});
onBeforeUpdate(() => {
  console.log(`componentC: before update!`);
});
onUpdated(() => {
  console.log(`componentC: updated!`);
});
onBeforeUnmount(() => {
  console.log(`componentC: before unmount!`);
});
onUnmounted(() => {
  console.log(`componentC: unmounted!`);
});
</script>

<template>
  <div style="background-color: lightsteelblue; padding: 10px">
    <div style="font-size: 30px">componentC</div>
  </div>
</template>

觀察 Mount 掛載階段

頁面渲染完成,觀察初始掛載的順序:
1

image

beforeMount() 鉤子觸發順序:
Vue 會先從最外層組件開始進行掛載,依次進入每一層子組件。

  • 進入最外層 componentA,觸發 beforeMount()
  • 遇到 componentB,進入 componentB,觸發 beforeMount()
  • 遇到 componentC,進入 componentC,觸發 beforeMount()

由外到內。

onMounted() 鉤子觸發順序:
Vue 會最先掛載完最內層組件,再一次向外逐層掛載。

  • 先在 componentC 完成掛載,觸發 onMounted()
  • 返回到 componentB 完成掛載,觸發 onMounted()
  • 最後返回到 componentA 完成掛載,觸發 onMounted()

由內到外。

而這樣的掛載順序確保了:子組件在父組件掛載完成前,已經準備好,保持了組件的正確依賴。

觀察 Update 更新階段

目前組件初始掛載完畢,保持在掛載階段,當資料一變動,會觸發 Vue 的更新階段。

我們點擊 componentB 的計數器按鈕:

image

count 值變化,觸發資料更新!接著 Vue 會:

  • 觸發 componentBbeforeUpdate()
  • 更新資料和 DOM。
  • 觸發 componentBonUpdated()

觀察 Unmount 卸載階段

componentA 有按鈕綁定 v-if 控制 子組件 componentB 是否呈現。
> componentB 有按鈕綁定 v-if 控制 子組件 componentC 是否呈現。

此時我們點擊 componentA 的按鈕,將 componentB v-if 切換為 false,改為不顯示:

image

此時 Vue 會:

  • 觸發 componentA 的更新階段。
  • 遇到 componentB,進入 componentB 的卸載階段。
  • 遇到 componentC,進入 componentC 的卸載階段,完成卸載,觸發 onUnmounted()
  • 回到 componentB 完成卸載,觸發 onUnmounted()
  • 完成更新 componentA,觸發 onUpdated()

小結

在還沒有寫鐵人賽的時候,就有依稀聽過身邊的工程師朋友講過生命週期是必須要知道的,而小菜菜現在!終於有比較理解為什麼需要學它了~!
了解不同階段我們可以做什麼事情,真的很重要(讚讚 減少點出包機會 XD)

/images/emoticon/emoticon58.gif/images/emoticon/emoticon58.gif/images/emoticon/emoticon58.gif

Btw 訂好標題看一看突然覺得很適合✨(應該不會偏題吧)

範例 code ⬇️

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

參考資料


上一篇
欸你是要進 Vue 了沒? - Day23:Vue 表單輸入綁定之 v-model 有你在的宇宙一切都不麻煩了
下一篇
欸你是要進 Vue 了沒? - Day25:Vue 組件偵聽器之 watch && watchEffect 是在襪取什麼東東
系列文
欸你是要進 Vue 了沒?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言