Vue3 – 指令(Directives)之 Lifecycle Hooks(mounted、updated、unmounted)
簡介
在 Vue 3 中,自訂指令(custom directives)提供了在 DOM 元素上直接掛鉤行為的能力。雖然 Vue 內建的 v-model、v-show、v-if 等指令已經涵蓋了大多數日常需求,但在面對特殊 UI 效果、第三方套件整合或是需要手動操作 DOM 的情境時,自訂指令仍是不可或缺的工具。
指令的 Lifecycle Hooks(mounted、updated、unmounted)就像元件的生命週期一樣,讓開發者可以在 元素被插入、更新、移除 的不同時機點執行自訂程式碼。正確掌握這三個 hook,能讓你的指令在效能、可維護性以及錯誤排除上都有更好的表現。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,循序漸進帶你了解如何在 Vue 3 中編寫安全、有效的指令生命週期。
核心概念
1. 指令的基本結構
在 Vue 3 中,註冊全域指令的語法如下:
import { createApp } from 'vue'
const app = createApp({ /* root component */ })
app.directive('my-directive', {
// 生命週期 Hook
mounted(el, binding, vnode, prevVnode) { /* ... */ },
updated(el, binding, vnode, prevVnode) { /* ... */ },
unmounted(el, binding, vnode, prevVnode) { /* ... */ }
})
| Hook | 何時觸發 | 主要用途 |
|---|---|---|
| mounted | 元素第一次掛載到實際的 DOM 上時 | 初始化第三方套件、設定事件監聽、計算尺寸等 |
| updated | 相關的 binding.value 或 VNode 更新後 |
依據新值重新渲染、調整樣式、重新綁定事件 |
| unmounted | 元素從 DOM 中移除前 | 清理資源、移除事件監聽、銷毀第三方實例 |
Tip:在 Vue 3 中,
beforeMount、beforeUpdate、beforeUnmount已被移除,僅保留上述三個 hooks,讓 API 更簡潔。
2. mounted – 為元素注入行為
mounted 只會在 第一次 把元素插入真實 DOM 時執行一次。這裡常見的需求包括:
- 初始化 第三方 UI 套件(如 Swiper、Chart.js)
- 設定 事件監聽(scroll、resize、keyup…)
- 讀取元素尺寸或位置(
getBoundingClientRect)
範例 1:在指令中啟動 Swiper
app.directive('swiper', {
mounted(el) {
// Swiper 必須在真實 DOM 上才能正確初始化
el.swiper = new Swiper(el, {
loop: true,
pagination: { el: '.swiper-pagination' }
})
},
unmounted(el) {
// 銷毀實例以免記憶體泄漏
el.swiper.destroy()
delete el.swiper
}
})
說明:將 Swiper 實例掛在
el上,方便在unmounted時直接取用並銷毀。
3. updated – 回應值變化
updated 會在 綁定值(binding.value)或相關的 VNode 更新後呼叫。常用於:
- 重新計算 UI(如根據新資料改變高度)
- 切換樣式或 class(根據布林值開關)
- 重新綁定 事件(若事件參數依賴於最新的資料)
範例 2:根據布林值切換 tooltip
app.directive('tooltip', {
mounted(el, binding) {
// 初始建立 tooltip
el._tooltip = createTooltip(el, binding.value)
},
updated(el, binding) {
// 當傳入的文字改變時,更新內容
if (binding.value !== binding.oldValue) {
el._tooltip.updateContent(binding.value)
}
},
unmounted(el) {
el._tooltip.destroy()
delete el._tooltip
}
})
說明:
binding.oldValue讓我們可以判斷內容是否真的改變,避免不必要的 DOM 操作。
4. unmounted – 清理資源
指令掛載的任何副作用(事件、計時器、第三方實例)都應在 unmounted 時釋放,否則會造成 記憶體泄漏 或 意外觸發。
範例 3:自動偵測視窗尺寸變化
app.directive('resize-observer', {
mounted(el, binding) {
const callback = typeof binding.value === 'function'
? binding.value
: () => {}
// 使用 ResizeObserver 監測尺寸
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
callback(entry.contentRect)
}
})
observer.observe(el)
// 存在 el 上,方便 later cleanup
el._resizeObserver = observer
},
unmounted(el) {
if (el._resizeObserver) {
el._resizeObserver.disconnect()
delete el._resizeObserver
}
}
})
5. 進階範例:結合 mounted、updated、unmounted 完成「懶加載圖片」指令
app.directive('lazy-img', {
// 1. 先在 mounted 時放置占位圖
mounted(el, binding) {
el._src = binding.value // 真正的圖片網址
el.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==' // 1x1 透明圖
el.dataset.loaded = 'false'
// 使用 IntersectionObserver 判斷是否進入視窗
const onIntersect = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting && el.dataset.loaded === 'false') {
el.src = el._src
el.dataset.loaded = 'true'
observer.unobserve(el) // 已載入即停止觀察
}
})
}
const observer = new IntersectionObserver(onIntersect, {
rootMargin: '50px' // 提前 50px 載入
})
observer.observe(el)
el._observer = observer
},
// 2. 若綁定值改變(例如切換圖片),重新載入
updated(el, binding) {
if (binding.value !== binding.oldValue) {
el._src = binding.value
el.dataset.loaded = 'false'
el.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
el._observer.observe(el) // 再次觀察
}
},
// 3. 清除 observer,防止記憶體泄漏
unmounted(el) {
if (el._observer) {
el._observer.disconnect()
delete el._observer
}
}
})
關鍵點:
mounted建立一次性的IntersectionObserver。updated檢查新舊值,若圖片 URL 改變則重新觸發載入流程。unmounted必須斷開 observer,否則即使元素已被移除,仍會持續觸發回呼。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
在 mounted 內直接修改 binding.value |
binding.value 為唯讀,直接改動會導致 Vue 警告且不會觸發重新渲染。 |
若需要變更外部資料,應透過 emit 或 ref 由父層更新。 |
忘記在 unmounted 清理事件 |
事件監聽器若未移除,會在組件銷毀後仍持續觸發,造成記憶體泄漏或錯誤。 | 在 unmounted 中使用 removeEventListener,或把監聽器存於 el._handler 方便清除。 |
在 updated 中執行重度計算 |
每次更新都會跑一次,若計算量大會拖慢渲染。 | 使用 防抖(debounce) 或 節流(throttle),僅在值真正變化時才執行。 |
在 mounted 中使用 setTimeout |
若組件在 timeout 前被銷毀,callback 仍會執行,可能操作已不存在的 DOM。 | 把 timer ID 存在 el._timer,在 unmounted 時 clearTimeout。 |
| 使用全域指令導致命名衝突 | 多個套件或團隊同時註冊相同名稱的指令,會互相覆蓋。 | 盡量使用 命名空間(如 v-app-tooltip)或改為 局部指令。 |
最佳實踐小結
- 只在需要時使用指令:若可以用組件或純 CSS 完成,盡量避免額外的指令。
- 將副作用限制在 hook 內:不要在指令外部留存全域變數或狀態。
- 保持 Hook 的單一職責:
mounted只負責初始化,updated只負責回應變化,unmounted只負責清理。 - 使用
el作為儲存容器:將第三方實例、observer、timer 等掛在el上,方便在unmounted時取得。 - 加入容錯檢查:例如
if (!el._observer) return,避免在銷毀階段因意外狀態拋錯。
實際應用場景
| 場景 | 為何適合使用指令 | 可能的 Hook 配置 |
|---|---|---|
| 圖片懶加載 | 需要在元素出現在視窗時才載入圖片,且需自動解除監聽 | mounted → 建立 IntersectionObserver updated → 處理 URL 變更 unmounted → disconnect |
| 第三方圖表(Chart.js) | 圖表必須在真實 DOM 上渲染,且在資料更新時重新繪製 | mounted → 初始化圖表 updated → chart.update() unmounted → chart.destroy() |
| 自訂滾動偵測 | 需要在特定元素滾動時觸發回呼,且在元素離開時釋放監聽 | mounted → addEventListener('scroll', handler) updated → 若回呼函式變更重新綁定 unmounted → removeEventListener |
| 動態表格列高度自動調整 | 列高依資料長度變化,需在每次資料變更時重新計算 | mounted → 初始計算高度 updated → 重新測量並設定 style.height unmounted → 清除任何 ResizeObserver |
| 表單驗證提示 | 依據欄位值即時顯示/隱藏錯誤訊息 | mounted → 監聽 input 事件 updated → 若驗證規則變更即時更新提示 unmounted → 移除事件監聽 |
總結
Vue 3 的指令生命週期 mounted、updated、unmounted 為開發者提供了在 DOM 插入、變更、移除 三個關鍵時刻執行自訂邏輯的機會。透過正確的 Hook 使用,我們可以:
- 安全地初始化 第三方套件或事件監聽(
mounted) - 即時回應 綁定值或 VNode 的變化(
updated) - 徹底清理 所有副作用,避免記憶體泄漏或意外觸發(
unmounted)
在實務開發中,將指令的職責保持「單一、可預測」是提升代碼可維護性的關鍵;同時,遵守最佳實踐(如在 el 上暫存實例、在 unmounted 完整釋放資源)能讓專案在規模擴大後依然保持良好效能。
掌握這三個 Hook 後,你就能在 Vue 3 中自如地打造 高效、可重用且易於除錯 的自訂指令,為你的前端專案增添彈性與力量。祝開發順利,玩得開心! 🚀