本文 AI 產出,尚未審核

Vue3 – 指令(Directives)之 Lifecycle Hooks(mounted、updated、unmounted)


簡介

在 Vue 3 中,自訂指令(custom directives)提供了在 DOM 元素上直接掛鉤行為的能力。雖然 Vue 內建的 v-modelv-showv-if 等指令已經涵蓋了大多數日常需求,但在面對特殊 UI 效果、第三方套件整合或是需要手動操作 DOM 的情境時,自訂指令仍是不可或缺的工具。

指令的 Lifecycle Hooksmountedupdatedunmounted)就像元件的生命週期一樣,讓開發者可以在 元素被插入、更新、移除 的不同時機點執行自訂程式碼。正確掌握這三個 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.valueVNode 更新後 依據新值重新渲染、調整樣式、重新綁定事件
unmounted 元素從 DOM 中移除前 清理資源、移除事件監聽、銷毀第三方實例

Tip:在 Vue 3 中,beforeMountbeforeUpdatebeforeUnmount 已被移除,僅保留上述三個 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. 進階範例:結合 mountedupdatedunmounted 完成「懶加載圖片」指令

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 警告且不會觸發重新渲染。 若需要變更外部資料,應透過 emitref 由父層更新。
忘記在 unmounted 清理事件 事件監聽器若未移除,會在組件銷毀後仍持續觸發,造成記憶體泄漏或錯誤。 unmounted 中使用 removeEventListener,或把監聽器存於 el._handler 方便清除。
updated 中執行重度計算 每次更新都會跑一次,若計算量大會拖慢渲染。 使用 防抖(debounce)節流(throttle),僅在值真正變化時才執行。
mounted 中使用 setTimeout 若組件在 timeout 前被銷毀,callback 仍會執行,可能操作已不存在的 DOM。 把 timer ID 存在 el._timer,在 unmountedclearTimeout
使用全域指令導致命名衝突 多個套件或團隊同時註冊相同名稱的指令,會互相覆蓋。 盡量使用 命名空間(如 v-app-tooltip)或改為 局部指令

最佳實踐小結

  1. 只在需要時使用指令:若可以用組件或純 CSS 完成,盡量避免額外的指令。
  2. 將副作用限制在 hook 內:不要在指令外部留存全域變數或狀態。
  3. 保持 Hook 的單一職責mounted 只負責初始化,updated 只負責回應變化,unmounted 只負責清理。
  4. 使用 el 作為儲存容器:將第三方實例、observer、timer 等掛在 el 上,方便在 unmounted 時取得。
  5. 加入容錯檢查:例如 if (!el._observer) return,避免在銷毀階段因意外狀態拋錯。

實際應用場景

場景 為何適合使用指令 可能的 Hook 配置
圖片懶加載 需要在元素出現在視窗時才載入圖片,且需自動解除監聽 mounted → 建立 IntersectionObserver
updated → 處理 URL 變更
unmounteddisconnect
第三方圖表(Chart.js) 圖表必須在真實 DOM 上渲染,且在資料更新時重新繪製 mounted → 初始化圖表
updatedchart.update()
unmountedchart.destroy()
自訂滾動偵測 需要在特定元素滾動時觸發回呼,且在元素離開時釋放監聽 mountedaddEventListener('scroll', handler)
updated → 若回呼函式變更重新綁定
unmountedremoveEventListener
動態表格列高度自動調整 列高依資料長度變化,需在每次資料變更時重新計算 mounted → 初始計算高度
updated → 重新測量並設定 style.height
unmounted → 清除任何 ResizeObserver
表單驗證提示 依據欄位值即時顯示/隱藏錯誤訊息 mounted → 監聽 input 事件
updated → 若驗證規則變更即時更新提示
unmounted → 移除事件監聽

總結

Vue 3 的指令生命週期 mountedupdatedunmounted 為開發者提供了在 DOM 插入、變更、移除 三個關鍵時刻執行自訂邏輯的機會。透過正確的 Hook 使用,我們可以:

  • 安全地初始化 第三方套件或事件監聽(mounted
  • 即時回應 綁定值或 VNode 的變化(updated
  • 徹底清理 所有副作用,避免記憶體泄漏或意外觸發(unmounted

在實務開發中,將指令的職責保持「單一、可預測」是提升代碼可維護性的關鍵;同時,遵守最佳實踐(如在 el 上暫存實例、在 unmounted 完整釋放資源)能讓專案在規模擴大後依然保持良好效能。

掌握這三個 Hook 後,你就能在 Vue 3 中自如地打造 高效、可重用且易於除錯 的自訂指令,為你的前端專案增添彈性與力量。祝開發順利,玩得開心! 🚀