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>

執行順序:

  1. 初始載入:setuponBeforeMountonMounted
  2. 點擊按鈕:onBeforeUpdateonUpdated
  3. 元件卸載:onBeforeUnmountonUnmounted

其他生命週期 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 APIOptions API
setup()beforeCreate + created
onBeforeMount()beforeMount
onMounted()mounted
onBeforeUpdate()beforeUpdate
onUpdated()updated
onBeforeUnmount()beforeUnmount
onUnmounted()unmounted
onErrorCaptured()errorCaptured
onActivated()activated
onDeactivated()deactivated

最佳實踐

  1. 在 onMounted 中發送 API 請求:確保元件已準備好接收資料

  2. 在 onUnmounted 中清理資源:移除事件監聽器、清除計時器、取消訂閱

  3. 避免在 onUpdated 中修改響應式資料:可能導致無限循環

  4. 使用 Composables 封裝生命週期邏輯:提高程式碼重用性

  5. 注意 async setup:如果 setup 是 async,需要配合 <Suspense> 使用