Vue3 模板語法:指令(Directives)概念
簡介
在 Vue3 中,**指令(Directives)**是連結 HTML 模板 與 Vue 實例 的橋樑。透過指令,我們可以在 DOM 元素上宣告式地綁定行為、屬性或事件,而不需要手動操作原生 DOM。指令的設計初衷是讓 UI 的描述保持 簡潔、可讀,同時保留 高彈性 的程式邏輯。
對於剛踏入前端框架的開發者而言,熟悉指令的使用與原理是掌握 Vue3 模板語法 的關鍵;對於已有一定經驗的開發者,深入了解內建指令的細節與自訂指令的編寫,則能在大型專案中提升 可維護性 與 開發效率。本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你完整掌握 Vue3 指令的運用。
核心概念
1. 什麼是指令?
Vue 的指令是一種以 v- 為前綴的特殊屬性,語法形式為 v-指令名稱:參數="表達式"。在編譯階段,Vue 會將指令轉換成對應的 渲染函式,於資料變更時自動更新 DOM。
- 內建指令:
v-bind、v-on、v-if、v-for、v-show、v-model、v-text、v-html等。 - 自訂指令:開發者可以透過
app.directive()註冊自己的指令,以實作特殊的 DOM 行為(如聚焦、拖曳等)。
2. 生命週期鉤子
指令在綁定到元素時會依序觸發以下鉤子,了解它們有助於寫出正確且效能佳的自訂指令:
| 鉤子 | 說明 |
|---|---|
created |
指令第一次綁定到元素時呼叫,此時尚未掛載到 DOM。 |
beforeMount |
元素即將插入父節點前呼叫。 |
mounted |
元素已插入 DOM,適合執行需要 DOM 的操作(如 focus、addEventListener)。 |
beforeUpdate |
資料變更導致指令更新前呼叫。 |
updated |
元素更新完畢後呼叫。 |
beforeUnmount |
元素即將被移除前呼叫。 |
unmounted |
元素已從 DOM 移除,適合清理資源(如移除監聽器)。 |
3. 常用內建指令解析
3.1 v-bind(簡寫 :)
將資料綁定到 HTML 屬性。支援 動態屬性名稱、多屬性一次綁定(v-bind="obj")。
<!-- 動態綁定 class -->
<div :class="{ active: isActive, disabled: isDisabled }"></div>
<!-- 一次綁定多個屬性 -->
<button v-bind="buttonAttrs">送出</button>
3.2 v-on(簡寫 @)
為元素添加 事件監聽器,支援 修飾符(.stop、.prevent、.once、.capture 等)以及 事件參數。
<!-- 點擊事件,阻止預設行為 -->
<a href="https://example.com" @click.prevent="goHome">回首頁</a>
<!-- 只觸發一次的自訂事件 -->
<button @submit.once="handleSubmit">送出</button>
3.3 v-if / v-else-if / v-else
根據條件 有條件地渲染 整個元素與其子樹。與 v-show 的差異在於 v-if 會 真正移除/新增 DOM,適合切換頻率較低的情況。
<p v-if="status === 'loading'">載入中…</p>
<p v-else-if="status === 'error'">發生錯誤</p>
<p v-else>資料已完成</p>
3.4 v-for
在 陣列或物件 上迭代,產生多筆相同結構的元素。常與 :key 搭配,提升重新渲染效能。
<ul>
<li v-for="(item, index) in list" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
</ul>
3.5 v-model
在 表單元件 上實現 雙向綁定。Vue3 支援 修飾符(.lazy、.number、.trim)以及 自訂指令(v-model:propName)以對應非原生的 value/input 事件。
<input v-model.trim="username" placeholder="請輸入使用者名稱" />
4. 自訂指令實作範例
以下示範三個常見的自訂指令:自動聚焦、拖曳、點擊外部關閉。每個範例皆說明生命週期鉤子與清理工作。
4.1 v-focus – 元素掛載後自動聚焦
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 註冊全域指令 v-focus
app.directive('focus', {
// 元素插入 DOM 後執行
mounted(el) {
el.focus()
}
})
app.mount('#app')
<!-- 使用方式 -->
<input type="text" v-focus placeholder="一進來就自動聚焦" />
重點:只需在
mounted鉤子聚焦即可,無需在unmounted做清理,因為聚焦不會留下副作用。
4.2 v-drag – 讓元素可拖曳
app.directive('drag', {
mounted(el) {
el.style.position = 'relative'
let startX, startY, initialX, initialY
const onMouseDown = (e) => {
startX = e.clientX
startY = e.clientY
const rect = el.getBoundingClientRect()
initialX = rect.left
initialY = rect.top
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
const onMouseMove = (e) => {
const dx = e.clientX - startX
const dy = e.clientY - startY
el.style.left = `${initialX + dx}px`
el.style.top = `${initialY + dy}px`
}
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
el.addEventListener('mousedown', onMouseDown)
},
// 清除事件監聽,避免記憶體洩漏
unmounted(el) {
el.removeEventListener('mousedown', onMouseDown)
}
})
<div v-drag class="box">拖我!</div>
<style>
.box {
width: 120px;
height: 80px;
background: #42b983;
color: #fff;
line-height: 80px;
text-align: center;
cursor: move;
}
</style>
最佳實踐:在
unmounted內務必移除所有在mounted時註冊的全域事件,否則會導致 記憶體洩漏。
4.3 v-click-outside – 點擊元素外部時觸發
app.directive('click-outside', {
beforeMount(el, binding) {
// 事件處理器需要保存於 el 上,才能在 unmounted 時正確移除
el.__vueClickOutside__ = (event) => {
if (!el.contains(event.target)) {
// 呼叫傳入的回呼函式
binding.value(event)
}
}
document.addEventListener('click', el.__vueClickOutside__)
},
unmounted(el) {
document.removeEventListener('click', el.__vueClickOutside__)
delete el.__vueClickOutside__
}
})
<template>
<div class="modal" v-if="show" v-click-outside="close">
<p>點擊外部即可關閉此對話框</p>
</div>
<button @click="show = true">開啟 Modal</button>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
const close = () => (show.value = false)
</script>
<style>
.modal {
position: fixed;
top: 30%;
left: 50%;
transform: translateX(-50%);
background: #fff;
padding: 20px;
border: 1px solid #ddd;
}
</style>
技巧:使用
el.__vueClickOutside__暴露給指令本身,避免在unmounted時找不到正確的參考。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 指令名稱拼寫錯誤 | Vue 只會辨識以 v- 開頭的指令,拼寫錯誤會變成普通屬性,導致無效。 |
使用 IDE 的自動補全或在 app.directive() 時統一命名。 |
忘記加 :key (在 v-for 中) |
沒有 key 會導致 Vue 以 最小化重排 的方式更新,可能產生錯誤的 UI 狀態。 |
為每個迭代項目提供唯一且穩定的 key(如資料庫 ID)。 |
在 v-if 中使用 v-model |
v-if 會移除整個節點,導致 v-model 的狀態被重置。 |
若需要保留狀態,改用 v-show 或將狀態提升至父層。 |
| 自訂指令未清理事件 | 事件監聽留在全域,會造成記憶體洩漏與意外觸發。 | 在 unmounted 鉤子中 一定 移除所有註冊的事件。 |
過度使用 v-html |
直接插入未經過濾的 HTML 會產生 XSS 風險。 | 僅在可信來源的內容使用,或透過 DOMPurify 之類的套件過濾。 |
在 v-bind 中使用函式 |
每次渲染都會重新建立函式,降低效能。 | 盡量使用 計算屬性 或 方法返回值,避免在模板內直接寫函式。 |
最佳實踐總結
- 保持指令簡潔:指令的職責應該單一,例如「聚焦」只做聚焦,「點擊外部」只負責偵測外部點擊。
- 遵循生命週期:所有需要操作 DOM 的行為放在
mounted,清理工作放在unmounted。 - 使用修飾符:合理利用
.stop、.prevent、.once等修飾符,減少程式碼冗餘。 - 避免過度嵌套
v-if/v-for:過深的條件與迭代會使模板難以閱讀與維護。可抽離成子元件或使用計算屬性預先整理資料。 - 型別安全:在
v-model上使用.number、.trim,確保取得的資料型別符合預期。
實際應用場景
| 場景 | 可能使用的指令 | 為什麼選擇它 |
|---|---|---|
| 表單驗證 | v-model、v-bind:class、v-on:submit.prevent |
雙向綁定即時取得輸入值,class 動態切換錯誤樣式,prevent 防止頁面重新整理。 |
| 彈出式選單 / Modal | v-if、v-show、v-click-outside(自訂) |
v-if 控制掛載與釋放,v-click-outside 處理點擊外部關閉的需求。 |
| 列表排序與拖曳 | v-for、v-drag(自訂) |
v-for 產生可拖曳的項目,v-drag 完成 UI 交互。 |
| 條件式載入資源 | v-if、v-bind:src、v-on:load |
只在需要時載入圖片或影片,load 事件可做懶加載或錯誤處理。 |
| 動態樣式切換 | v-bind:class、v-bind:style |
依據資料狀態即時變更 CSS 類別或行內樣式,實現主題切換、暗色模式等。 |
案例說明:假設在一個電商平台的商品列表頁,我們需要在使用者點擊「加入購物車」後顯示一個暫時的提示框。可以使用
v-show控制提示框的顯示(因為切換頻繁),同時搭配v-bind:class依據成功或失敗切換樣式。若提示框需要在點擊外部時自動關閉,則加入v-click-outside自訂指令即可完成整個需求。
總結
Vue3 的 指令 為模板提供了 宣告式、可組合 的方式來操作 DOM。透過 v-bind、v-on、v-if、v-for、v-model 等內建指令,我們可以快速完成屬性綁定、事件監聽、條件渲染與列表迭代等常見需求;而自訂指令則讓開發者在 特定 UI 行為(如聚焦、拖曳、點擊外部)上擁有更大的彈性。
在實務開發中,遵循指令的生命週期、妥善清理資源、合理使用修飾符與 key,是避免效能問題與記憶體洩漏的關鍵。結合上述概念與範例,你可以在 Vue3 專案裡以 簡潔、可讀、可維護 的方式編寫 UI,從而提升開發效率與使用者體驗。
祝你在 Vue3 的世界裡玩得開心,寫出更優雅的前端程式碼!