蛤肉,大家好。
如標題所述,今天我們要來看 Vue 中一個重要的觀念:「生命週期」。
我們將會分為以下幾部分來學習:
- 生命週期定義
- 為什麼需要了解?
- 初步概念
- 階段流程
- 組合式 API 鉤子函式
- 組件間的生命週期互動與觸發順序
OK 我們開始!(沒在要跟你喘一下)
生命週期(Lifecycle Hooks),可以理解為:組件從出生到死亡的完整過程。
從一個組件的實例被創建、初始化,到呈現在介面上,再經歷資料變動和更新,最後,當它不再被需要時,就會被銷毀。
這段過程中的每個階段,Vue 都會「執行對應的操作」,而理解各階段的行為,可以幫助我們在適當的時機點介入,實現我們想要做的事情。
在 Vue 的生命週期中,有這幾個階段可以注意:
每個階段的前、後都有相應的鉤子函式(hook functions),讓我們可以在這些階段前後取得資料或執行特定行為。
官方提供的圖很清楚~跟著步驟一步一步看看組件們在不同階段都會做些什麼吧!
圖片來源:官方文件
圖中紅色方框的階段為「鉤子函式(hook functions)」,可以理解為在不同生命週期階段可執行的 API 操作。
Vue 3 的組合式 API 沒有 Create
階段的對應鉤子函式,而是將初始化的邏輯集中到了 setup()
函式中執行,和 Vue 2 的選項式 API 不同,具體差異可以參考以下:
也就是流程圖的第一步:「渲染器遇到組件」。
只要遇到一個組件就會觸發。
也就是「渲染器遇到組件後~準備開始渲染之前」這個階段。
所有組件的數據都在這階段中「被初始化」,這確保了在渲染介面前準備好所有必要的資料。
生命週期的第一步是:執行組合式 API 中定義的 <script setup>
區塊,組合式 API 的實例會在這階段被創建。
選項式 API 的實例會在這階段被創建。
其中又區分為:
接下來進入讀取組件中的 <template>
,看組件中是否有 <template>
區塊。
<template>
,可能是用程式碼定義的操作(渲染函式寫的 HTML 結構、JSX、手動操作的 DOM 語法等等)。也就是渲染介面的時間點。
其中又區分為:
在完成第一次的渲染後,組件也就完成了初始掛載。
接著,組件將「保持在掛載階段中」(如下圖中的紅色圓圈的位置),直到銷毀前。
組件存在掛載階段中時,會有一個持續循環的過程,稱為更新階段(虛線圓圈的循環)。
這個過程中,因為資料變動,Vue 會重新渲染並打補丁(更新介面並 patch DOM 樹的過程),組件會一直循環這個過程,直到需要取消掛載。
當組件不再被需要時,就會進入卸載階段,準備被銷毀。
其中又區分為:
以上是主要的階段流程,接下來來看看 Vue 提供給組合式 API 使用的鉤子函式吧~
鉤子函式的參數是一個 callback function,在定義時進行註冊,並會在對應的生命週期階段自動 callback 執行。
今天會帶大家看這幾種:
- onBeforeMount()
- onMounted()
- onBeforeUpdate()
- onUpdated()
- onBeforeUnmount()
- onUnmounted()
會介紹它們個別
並會以範例帶大家比較使用時機~
注意:這幾種皆無法在服務器渲染 SSR 使用。
(在服務器渲染,伺服器主要負責生成靜態 HTML,再交到客戶端執行 DOM 操作,掛載階段是發生在客戶端,服務器端無法取得 DOM 元素,因此也無法執行完成掛載、更新、卸載相關的邏輯。)
分界點為掛載階段:
onBeforeMount()
onMounted()
官方文件:注意仅当根容器在文档中时,才可以保证组件 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
中的文字。
使用 onBeforeMount()
:
<script setup>
import { onMounted, onBeforeMount } from "vue";
onBeforeMount(() => {
console.log(document.getElementById("button").textContent);
});
</script>
<template>
<button id="button">Mount</button>
</template>
報錯了,因為頁面還沒有創建 DOM。
分界點為更新階段:
onBeforeUpdate()
onUpdated()
補充:Vue 的渲染機制,可在同一個組件更新不同的數據和 DOM,但只觸發一次 onUpdated()
。
所以官方建議:如果想在「特定」DOM 更新後執行某些操作,請使用 nextTick()。
我們設置一個計數器使用的情境來看看~
- 綁定計數器響應式狀態
count
。- 點擊按鈕觸發計數器資料更新。
- 將更新後的資料,同步介面。
我們分別在更新前、後,使用 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()
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
- componentA import componentB,
componentB import componentC。- componentB 有一個計數器,會更新數據。
- componentA 有按鈕綁定 v-if 控制 子組件 componentB 是否呈現。
- 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>
頁面渲染完成,觀察初始掛載的順序:
beforeMount()
鉤子觸發順序:
Vue 會先從最外層組件開始進行掛載,依次進入每一層子組件。
componentA
,觸發 beforeMount()
。componentB
,進入 componentB
,觸發 beforeMount()
。componentC
,進入 componentC
,觸發 beforeMount()
。由外到內。
onMounted()
鉤子觸發順序:
Vue 會最先掛載完最內層組件,再一次向外逐層掛載。
componentC
完成掛載,觸發 onMounted()
。componentB
完成掛載,觸發 onMounted()
。componentA
完成掛載,觸發 onMounted()
。由內到外。
而這樣的掛載順序確保了:子組件在父組件掛載完成前,已經準備好,保持了組件的正確依賴。
目前組件初始掛載完畢,保持在掛載階段,當資料一變動,會觸發 Vue 的更新階段。
我們點擊 componentB
的計數器按鈕:
count
值變化,觸發資料更新!接著 Vue 會:
componentB
的 beforeUpdate()
。componentB
的 onUpdated()
。componentA 有按鈕綁定 v-if 控制 子組件 componentB 是否呈現。
> componentB 有按鈕綁定 v-if 控制 子組件 componentC 是否呈現。
此時我們點擊 componentA
的按鈕,將 componentB
v-if
切換為 false
,改為不顯示:
此時 Vue 會:
componentA
的更新階段。componentB
,進入 componentB
的卸載階段。componentC
,進入 componentC
的卸載階段,完成卸載,觸發 onUnmounted()
。componentB
完成卸載,觸發 onUnmounted()
。componentA
,觸發 onUpdated()
。在還沒有寫鐵人賽的時候,就有依稀聽過身邊的工程師朋友講過生命週期是必須要知道的,而小菜菜現在!終於有比較理解為什麼需要學它了~!
了解不同階段我們可以做什麼事情,真的很重要(讚讚 減少點出包機會 XD)
Btw 訂好標題看一看突然覺得很適合聽✨(應該不會偏題吧)
https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day24