Skip to content

元件的生命週期

重新認識 Vue.js: 008 天絕對看不完的 Vue.js 3.x 指南

元件不是出現在畫面後就永遠留著。它會被建立、掛載、隨資料更新,也可能因為 v-if 或路由切換而被移除。這些固定階段,就是元件的生命週期 (lifecycle)。Vue 在每個階段都留了掛鉤 (hooks),讓我們的程式碼可以在正確的時間點介入:畫面就緒後啟動計時器、元件移除前把它清掉,都是生命週期的日常應用。

元件的一生

先建立整體圖像:

三個階段值得記住:

  • 掛載 (mount)<script setup> 的程式碼先執行,接著 Vue 建立元件的模板 DOM、放進頁面。
  • 更新 (update):響應式資料變化,Vue 重新渲染受影響的部分。這個循環在元件存活期間可能發生無數次。
  • 卸載 (unmount):元件從畫面上移除,例如 v-if 變成 false,或路由切換離開頁面。

六個一般客戶端 hooks

hooks 都是從 vue 匯入的函式,呼叫時傳入 callback,Vue 會在對應的時間點執行它:

Hook時間點
onBeforeMount掛載開始之前
onMounted掛載完成,模板 DOM 已建立並插入父容器
onBeforeUpdate資料變化後、DOM 更新前
onUpdatedDOM 更新完成
onBeforeUnmount卸載開始之前,元件仍完整可用
onUnmounted卸載完成

六個 hook 不必平均看待,實務上的出場頻率差距很大。與其背表格,不如先想「這件事需要什麼時機」,再挑對應的工具:

想做的事建議放哪裡
宣告狀態、computed、一般函式<script setup> 頂層
第一次渲染後存取 DOM、初始化需要 DOM 的套件onMounted
針對某筆資料的變化執行動作watch
等待某一次狀態變化造成的 DOM 更新await nextTick()(上一節提過)
清除計時器、事件監聽或連線onUnmounted
每次元件 DOM 更新後都執行onUpdated,但很少真的需要

值得注意的是最後一列:如果只是想等待「某一次」狀態變化造成的 DOM 更新,用 await nextTick(),不要攔截每一次更新的 onUpdated,這也是 Vue 官方的建議。

日常開發最常用的是 onMountedonUnmounted 這一對:前者啟動需要「畫面就緒」的工作,後者負責善後。用一個時鐘元件實際看:

src/components/LiveClock.vue

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const now = ref('')
let timer = null

function tick() {
  now.value = new Date().toLocaleTimeString()
}

onMounted(() => {
  tick()                              // 掛載完成、畫面就緒,才啟動計時器
  timer = setInterval(tick, 1000)
})

onUnmounted(() => {
  clearInterval(timer)                // 卸載時清掉,否則計時器會一直留著
})
</script>

<template>
  <p class="clock">現在時間:{{ now }}</p>
</template>

<style scoped>
.clock {
  font-variant-numeric: tabular-nums;
}
</style>

src/App.vue

vue
<script setup>
import { ref } from 'vue'
import LiveClock from './components/LiveClock.vue'

const visible = ref(true)
</script>

<template>
  <main class="app">
    <button type="button" @click="visible = !visible">
      {{ visible ? '隱藏' : '顯示' }}時鐘
    </button>
    <LiveClock v-if="visible" />
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}
</style>

按下「隱藏時鐘」後,v-if 會卸載元件,onUnmounted 也會跟著執行 clearInterval。這行不能省。

這裡有一條清楚的界線:元件卸載時,Vue 會自動停下這個元件的渲染更新、computedwatch,這些不用我們操心;但 setInterval、DOM 事件監聽、WebSocket 連線這類瀏覽器或外部資源,Vue 不知道它們的存在,不會替我們清理。少了清理,每顯示一次時鐘,就會多留下一個永遠不停的計時器,這是典型的記憶體洩漏 (memory leak) 來源。

簡單記:在哪個 hook 建立資源,就在對應的 hook 清掉。

掛載時序:什麼時候拿得到模板 DOM

<script setup> 的程式碼執行時,掛載還沒開始,元件自己的模板 DOM 這時候還不存在。所以任何需要碰觸自身 DOM 的工作(量尺寸、對焦輸入框、初始化需要 DOM 元素的第三方套件),都不能寫在 <script setup> 的頂層,要放進 onMounted

回頭看生命週期圖就很清楚:「建立元件的模板 DOM」發生在 setup 之後、onMounted 之前。onMounted 是第一個能保證「元件自己的 DOM 已建立並插入父容器」的時間點;如果應用程式的根容器本來就在頁面文件裡,元件 DOM 此時也會在頁面文件裡。

至於「拿到 DOM 之後怎麼操作」,Vue 提供了 template ref 這個正式管道,它同樣要等掛載完成後才可使用。這是第四章元件設計的內容,本章先把時序的觀念建立起來。

onMounted 不等於整個頁面都載入完成

onMounted 能保證元件自己的模板 DOM 已建立,也代表同步子元件已經掛載;但它不會等待圖片、API 請求、非同步元件或 <Suspense> 裡的內容全部完成。

所以它適合拿來存取 DOM,不適合當成「所有資料都準備好了」的訊號。

使用 hooks 的兩個規則

規則一:hooks 必須在 setup 執行期間同步註冊。 Vue 需要在元件建立時,就知道每個 hook 屬於哪個元件。因此 hooks 可以寫在 <script setup> 頂層,但不能等到 setTimeout、Promise callback 或使用者事件發生後才註冊,那時 setup 早已結束,Vue 對不上元件,hook 不會生效。

js
onMounted(() => { /* ... */ })          // 正確:setup 流程中同步註冊

setTimeout(() => {
  onMounted(() => { /* ... */ })        // 錯誤:註冊時機已離開 setup 流程
}, 1000)

規則二:不要在 onUpdated 裡修改響應式狀態。

常見錯誤:在 onUpdated 裡改資料,畫面陷入無限更新

onUpdated 在每次 DOM 更新完成後執行。如果在裡面又修改了響應式資料,就會觸發下一次更新,更新完又執行 onUpdated,再修改、再更新,元件從此在更新循環裡打轉,輕則效能異常,重則瀏覽器直接卡死。

如果目的是「依某筆資料變化做某件事」,該用的工具是上一章學過的 watch,它能明確指定觀察對象,而不是攔在每一次更新之後。

最後補充一句:生命週期 hooks 不只這六個,還有錯誤捕捉 (onErrorCaptured)、搭配 <KeepAlive> 的啟用停用 (onActivatedonDeactivated)、伺服器端渲染 (SSR) 專用等成員,它們各自屬於後面章節的主題,用到時再介紹。

這一章從 SFC 的結構、refreactive 的取捨,一路走到 props、emits、v-model 與生命週期。第四章我們把主題拉到元件設計:slots、provide/inject、template refs,讓元件之間的組合方式更豐富。