Vue 生命週期
每個 Vue 元件實例在被建立時都會經歷一系列的初始化過程,例如設置資料監聽、編譯模板、掛載實例到 DOM、以及在資料變化時更新 DOM。在這個過程中,Vue 會呼叫相應的生命週期鉤子(Lifecycle Hooks),讓你可以在特定時機執行自己的程式碼。
生命週期圖示
┌─────────────────────────────────────────────────────────┐
│ 建立階段 │
├─────────────────────────────────────────────────────────┤
│ setup() │
│ ↓ │
│ onBeforeMount() ← 元件即將被掛載到 DOM │
│ ↓ │
│ onMounted() ← 元件已掛載到 DOM(可以存取 DOM) │
├─────────────────────────────────────────────────────────┤
│ 更新階段 │
├─────────────────────────────────────────────────────────┤
│ onBeforeUpdate() ← 資料變化,DOM 更新前 │
│ ↓ │
│ onUpdated() ← DOM 已更新 │
├─────────────────────────────────────────────────────────┤
│ 卸載階段 │
├─────────────────────────────────────────────────────────┤
│ onBeforeUnmount() ← 元件即將被卸載 │
│ ↓ │
│ onUnmounted() ← 元件已卸載(清理資源) │
└─────────────────────────────────────────────────────────┘
生命週期 Hooks
在 Composition API 中,生命週期 hooks 以 on 開頭:
onMounted
元件被掛載到 DOM 後呼叫。這是最常用的 hook,適合:
- 存取 DOM 元素
- 發送 API 請求
- 設置監聽器(如 window resize)
- 初始化第三方函式庫
<script setup>
import { ref, onMounted } from 'vue'
const el = ref(null)
const data = ref(null)
onMounted(() => {
// DOM 已經存在,可以存取
console.log(el.value) // DOM 元素
// 發送 API 請求
fetch('/api/data')
.then(res => res.json())
.then(result => {
data.value = result
})
})
</script>
<template>
<div ref="el">Hello</div>
</template>
onUpdated
元件的響應式資料變化導致 DOM 更新後呼叫:
<script setup>
import { ref, onUpdated } from 'vue'
const count = ref(0)
onUpdated(() => {
// DOM 已更新
console.log('DOM updated, count is now:', count.value)
})
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
不要在
onUpdated 中修改響應式資料,否則可能導致無限循環!onUnmounted
元件被卸載後呼叫,適合清理資源:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const timer = ref(null)
onMounted(() => {
// 設置計時器
timer.value = setInterval(() => {
console.log('tick')
}, 1000)
})
onUnmounted(() => {
// 清除計時器
if (timer.value) {
clearInterval(timer.value)
}
})
</script>
onBeforeMount / onBeforeUpdate / onBeforeUnmount
這些 hooks 在對應的動作發生「之前」呼叫:
<script setup>
import { onBeforeMount, onBeforeUpdate, onBeforeUnmount } from 'vue'
onBeforeMount(() => {
console.log('元件即將掛載')
// 此時還無法存取 DOM
})
onBeforeUpdate(() => {
console.log('DOM 即將更新')
// 可以在這裡取得更新前的 DOM 狀態
})
onBeforeUnmount(() => {
console.log('元件即將卸載')
// 最後清理資源的機會
})
</script>
完整範例
<script setup>
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
const count = ref(0)
console.log('setup - 元件正在初始化')
onBeforeMount(() => {
console.log('onBeforeMount - 元件即將掛載')
})
onMounted(() => {
console.log('onMounted - 元件已掛載')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate - DOM 即將更新')
})
onUpdated(() => {
console.log('onUpdated - DOM 已更新')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount - 元件即將卸載')
})
onUnmounted(() => {
console.log('onUnmounted - 元件已卸載')
})
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
執行順序:
- 初始載入:
setup→onBeforeMount→onMounted - 點擊按鈕:
onBeforeUpdate→onUpdated - 元件卸載:
onBeforeUnmount→onUnmounted
其他生命週期 Hooks
onErrorCaptured
捕獲來自後代元件的錯誤:
<script setup>
import { onErrorCaptured } from 'vue'
onErrorCaptured((error, instance, info) => {
console.error('捕獲到錯誤:', error)
console.log('錯誤來自:', instance)
console.log('錯誤資訊:', info)
// 返回 false 阻止錯誤繼續向上傳播
return false
})
</script>
onActivated / onDeactivated
當元件被 <KeepAlive> 快取時使用:
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('元件被啟用(從快取中恢復)')
// 重新獲取資料或恢復狀態
})
onDeactivated(() => {
console.log('元件被停用(進入快取)')
// 暫停計時器或清理臨時狀態
})
</script>
onRenderTracked / onRenderTriggered(偵錯用)
這兩個 hooks 只在開發模式下可用,用於偵錯響應式依賴:
<script setup>
import { ref, onRenderTracked, onRenderTriggered } from 'vue'
const count = ref(0)
onRenderTracked((event) => {
console.log('追蹤到依賴:', event)
})
onRenderTriggered((event) => {
console.log('觸發重新渲染:', event)
})
</script>
實際應用範例
滾動位置記憶
<script setup>
import { ref, onMounted, onUnmounted, onActivated } from 'vue'
const scrollPosition = ref(0)
function handleScroll() {
scrollPosition.value = window.scrollY
}
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
// 如果使用 KeepAlive,恢復滾動位置
onActivated(() => {
window.scrollTo(0, scrollPosition.value)
})
</script>
第三方函式庫整合
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const chartRef = ref(null)
let chartInstance = null
onMounted(() => {
// 初始化第三方圖表庫
chartInstance = new Chart(chartRef.value, {
// ... 設定
})
})
onUnmounted(() => {
// 銷毀實例,避免記憶體洩漏
if (chartInstance) {
chartInstance.destroy()
}
})
</script>
<template>
<canvas ref="chartRef"></canvas>
</template>
視窗大小監聽
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const windowWidth = ref(window.innerWidth)
const windowHeight = ref(window.innerHeight)
function handleResize() {
windowWidth.value = window.innerWidth
windowHeight.value = window.innerHeight
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<p>視窗大小:{{ windowWidth }} x {{ windowHeight }}</p>
</template>
資料載入
<script setup>
import { ref, onMounted } from 'vue'
const user = ref(null)
const isLoading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const response = await fetch('/api/user')
if (!response.ok) throw new Error('載入失敗')
user.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
})
</script>
<template>
<div v-if="isLoading">載入中...</div>
<div v-else-if="error">錯誤:{{ error }}</div>
<div v-else>
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
</div>
</template>
自訂 Composable 中使用生命週期
// useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
function update() {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => window.addEventListener('resize', update))
onUnmounted(() => window.removeEventListener('resize', update))
return { width, height }
}
<script setup>
import { useWindowSize } from './useWindowSize'
const { width, height } = useWindowSize()
</script>
<template>
<p>{{ width }} x {{ height }}</p>
</template>
Options API 對照
如果你看到舊的程式碼使用 Options API,以下是對照表:
| Composition API | Options API |
|---|---|
setup() | beforeCreate + created |
onBeforeMount() | beforeMount |
onMounted() | mounted |
onBeforeUpdate() | beforeUpdate |
onUpdated() | updated |
onBeforeUnmount() | beforeUnmount |
onUnmounted() | unmounted |
onErrorCaptured() | errorCaptured |
onActivated() | activated |
onDeactivated() | deactivated |
最佳實踐
在 onMounted 中發送 API 請求:確保元件已準備好接收資料
在 onUnmounted 中清理資源:移除事件監聽器、清除計時器、取消訂閱
避免在 onUpdated 中修改響應式資料:可能導致無限循環
使用 Composables 封裝生命週期邏輯:提高程式碼重用性
注意 async setup:如果 setup 是 async,需要配合
<Suspense>使用