Vue3 – Lifecycle Hooks(生命週期)
主題:setup() 之前與之後
簡介
在 Vue 3 中,Composition API 以 setup() 為核心,取代了 Options API 中的多數生命週期寫法。了解 setup() 前後會觸發哪些 hook,才能在正確的時機取得資料、掛載事件或釋放資源。
如果把 setup() 想成「元件的建構子」,那麼它前面的 hook(beforeCreate、created)是元件「還未完成初始化」的階段;setup() 後的 hook(onBeforeMount、onMounted、onBeforeUnmount…)則是「DOM 已經可操作」或「即將被銷毀」的階段。掌握這些時序,有助於:
- 避免在錯誤的時機存取 DOM(會得到
null)。 - 正確地初始化或清理副作用(如訂閱、計時器)。
- 提升元件的可測試性與可維護性,因為副作用集中在明確的 hook 中。
以下內容會說明 setup() 前後的生命週期順序、實作方式以及常見陷阱,並提供多個實用範例,讓你在開發 Vue 3 應用時,能夠得心應手。
核心概念
1. 生命週期的完整順序(含 setup())
| 時間點 | Options API Hook | Composition API 等價 | 說明 |
|---|---|---|---|
| 1 | beforeCreate |
無直接等價(可用 setup() 前的程式碼) |
元件實例已建立,但 data、props、methods 尚未初始化。 |
| 2 | setup() |
setup() |
初始化 props、emit,建立 reactive state,返回要暴露給模板的值。 |
| 3 | created |
onCreated(已廢除) → 無 |
setup() 結束後,data、computed、watch 已建立,可直接使用 this(僅 Options API)。 |
| 4 | beforeMount |
onBeforeMount |
模板已編譯,但尚未掛載到真實 DOM。 |
| 5 | mounted |
onMounted |
真實 DOM 已插入,這裡可以安全地操作 DOM、註冊第三方插件。 |
| 6 | beforeUpdate |
onBeforeUpdate |
响應式資料變更後,DOM 重新渲染前觸發。 |
| 7 | updated |
onUpdated |
DOM 更新完畢。 |
| 8 | beforeUnmount |
onBeforeUnmount |
元件即將被銷毀,適合清理資源。 |
| 9 | unmounted |
onUnmounted |
元件已從 DOM 移除,最後的清理時機。 |
重點:
setup()介於beforeCreate與created之間,且在所有其他 hook 之前執行。這意味著在setup()內,你只能使用props、emit、expose,而不能使用this(因為實例尚未完成)。
2. setup() 前的 Hook:beforeCreate
beforeCreate 在 Vue 3 中仍然存在,但在 Composition API 中很少直接使用。若你仍需要在 setup() 前執行程式碼(例如全局的插件初始化),可以在 beforeCreate 中完成:
export default {
beforeCreate() {
console.log('元件即將建立,this.$options =', this.$options)
// 例如:設定全域的錯誤處理器
this.$options.errorHandler = (err, vm, info) => {
console.error('全域錯誤', err, info)
}
}
}
注意:此時 props 尚未解析,data、computed 也不存在,僅能存取靜態的 component options。
3. setup() 本身的特性
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
export default {
// props 由外部傳入
props: {
msg: String,
delay: { type: Number, default: 1000 }
},
// 這裡是 Composition API 的入口
setup(props, { emit, expose }) {
// 1️⃣ 只能透過參數取得 props,不能使用 this.msg
const count = ref(0)
// 2️⃣ computed 必須在 setup 內建立
const doubled = computed(() => count.value * 2)
// 3️⃣ 訂閱外部事件或啟動計時器,通常放在 onMounted
const startTimer = () => {
const timer = setInterval(() => {
count.value++
emit('update', count.value) // 向父層傳遞變化
}, props.delay)
// onBeforeUnmount 時清除
onBeforeUnmount(() => clearInterval(timer))
}
// 4️⃣ onMounted 只會在 DOM 完成掛載後執行
onMounted(() => {
console.log('DOM 已掛載,第一次 count =', count.value)
startTimer()
})
// 5️⃣ expose 讓父層能直接呼叫子層的方法
expose({
reset() {
count.value = 0
}
})
// 返回給模板使用的資料
return { count, doubled }
}
}
重點說明:
props只能透過參數取得,this在setup()中是undefined。- 返回的物件會自動被
proxy包裝,在模板中直接使用(如{{ count }})。 onMounted、onBeforeUnmount等 hook 必須在setup()內呼叫,才能正確註冊。
4. setup() 後的 Hook:onMounted、onBeforeUnmount…
以下示範在 setup() 之後,如何利用 onMounted 取得真實 DOM,並在 onBeforeUnmount 釋放資源:
import { ref, onMounted, onBeforeUnmount } from 'vue'
export default {
setup() {
const inputEl = ref(null) // 透過 template ref 取得 DOM
const focusInput = () => {
if (inputEl.value) inputEl.value.focus()
}
// 元件掛載完畢,執行一次自動聚焦
onMounted(() => {
console.log('元件已掛載')
focusInput()
})
// 元件即將銷毀,移除事件監聽
onBeforeUnmount(() => {
console.log('元件即將被銷毀')
// 假設我們在 onMounted 時加了 window 事件
window.removeEventListener('resize', onResize)
})
// 只要在 setup 中返回,就能在模板使用
return { inputEl, focusInput }
},
template: `
<input ref="inputEl" @keyup.enter="focusInput" />
<button @click="focusInput">Focus Input</button>
`
}
5. 進階範例:watch、watchEffect 與 onBeforeUpdate
import { ref, watch, watchEffect, onBeforeUpdate } from 'vue'
export default {
props: { url: String },
setup(props) {
const data = ref(null)
const loading = ref(false)
// 1️⃣ 使用 watch 監聽 props.url 變化,重新抓取資料
watch(
() => props.url,
async (newUrl, oldUrl) => {
loading.value = true
const res = await fetch(newUrl)
data.value = await res.json()
loading.value = false
},
{ immediate: true } // 立即執行一次
)
// 2️⃣ watchEffect 會在任何被依賴的響應式變數變化時重新執行
watchEffect(() => {
console.log('data 變化了:', data.value)
})
// 3️⃣ onBeforeUpdate 可用於偵測 DOM 重新渲染前的最後一次資料狀態
onBeforeUpdate(() => {
console.log('即將更新,當前 count =', data.value?.length)
})
return { data, loading }
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 / 最佳實踐 |
|---|---|---|
在 setup() 中使用 this |
this 此時指向 undefined,會拋出錯誤。 |
改用參數 props、emit,或 getCurrentInstance() 取得實例。 |
在 setup() 內直接呼叫 mounted 相關 API(如 document.querySelector) |
DOM 尚未掛載,會得到 null。 |
把 DOM 相關程式碼搬到 onMounted。 |
忘記在 onBeforeUnmount 清除計時器或事件監聽 |
記憶體泄漏、意外觸發多次回呼。 | 始終 在相對應的「銷毀」hook 中釋放資源。 |
watch 內部使用非同步函式但未處理錯誤 |
錯誤會沉默,導致 UI 卡住。 | 使用 try / catch 或外層的錯誤捕捉機制。 |
過度在 setup() 中寫業務邏輯 |
使 setup() 變得龐大且難以測試。 |
把邏輯抽成 Composable(如 useFetch、useTimer),保持 setup() 輕量。 |
在 setup() 內返回非響應式值 |
雖然可以,但在模板中不會觸發更新。 | 若需要自動更新,使用 ref、reactive、computed。 |
最佳實踐總結:
- 僅在
setup()中建立 reactive state、computed、watch、hook 註冊。 - 所有與 DOM 互動的程式碼放在
onMounted(或onBeforeUpdate)。 - 資源釋放統一放在
onBeforeUnmount/onUnmounted。 - 使用 composable 把可重用邏輯抽離,提升可測試性。
- 避免在
setup()中使用this,改以參數或getCurrentInstance()。
實際應用場景
| 場景 | 為何需要掌握 setup() 前後的 Hook |
典型實作方式 |
|---|---|---|
| 表單驗證與自動聚焦 | 必須在 DOM 完成掛載後取得 <input> 元素,才能呼叫 .focus()。 |
在 setup() 中建立 ref,於 onMounted 呼叫 inputRef.value?.focus()。 |
| 即時資料串流(WebSocket) | 連線應在元件建立時就發起,但必須在元件銷毀時關閉。 | 在 setup() 中建立 socket,於 onMounted socket.connect(),於 onBeforeUnmount socket.disconnect()。 |
| 路由參數變化重新抓取資料 | props(如 route.params.id)變化時,需要重新發送請求。 |
使用 watch(() => props.id, fetchData, { immediate:true }),確保在 setup() 後即時觸發。 |
| 動畫與第三方 UI 套件 | 許多套件需要取得真實 DOM 才能初始化(如 Swiper、GSAP)。 | 在 setup() 中宣告 ref,於 onMounted 初始化套件;於 onBeforeUnmount 銷毀。 |
| 全域狀態同步(Pinia/Vuex) | 需要在元件掛載時自動註冊 store,並在卸載時取消訂閱。 | setup() 內 const store = useStore(),onMounted store.subscribe(...),onBeforeUnmount 取消。 |
總結
setup()是 Vue 3 Composition API 的入口,位於beforeCreate與created之間。- 在
setup()前只能取得靜態的 component options,無法操作this、DOM 或 reactive 資料。 - 在
setup()內:建立ref、reactive、computed、watch,以及註冊生命週期 hook(onMounted、onBeforeUnmount…)。 - 在
setup()後:利用onMounted取得真實 DOM、啟動第三方插件;利用onBeforeUnmount/onUnmounted釋放資源。 - 常見陷阱包括誤用
this、提前操作 DOM、忘記清理副作用。遵守「建立 → 使用 → 清理」的三步走原則,可讓元件更安全、易維護。 - 把可重用的邏輯抽成 Composable,讓
setup()保持簡潔,提升測試與團隊協作效率。
掌握 setup() 前後的生命週期時序,你就能在 Vue 3 中寫出 結構清晰、效能佳、易於維護 的元件程式碼。祝開發順利!