前言
在前几篇博客中,我们聊了 Vue 项目的脚手架选型、环境管理、VSCode 配置以及代码规范。今天,我们把目光投向 Vue 最核心的语法元素------指令(Directives)。
指令是 Vue 模板语法的灵魂。它们以 v- 前缀的特殊属性形式存在,让静态的 HTML 拥有了动态渲染、事件处理、数据绑定等能力。可以说,掌握了指令,就掌握了 Vue 开发的 80%。
本文将系统梳理 Vue 的所有核心指令,并重点对比 Vue 2 和 Vue 3 在指令层面的差异与演进。通过大量的代码案例和真实项目场景,帮助你建立完整的指令知识体系。
一、指令是什么?
在 Vue 中,指令(Directives) 是带有 v- 前缀的特殊 attribute。指令的职责是,当表达式的值发生变化时,响应式地作用于 DOM。
html
<!-- 当 message 变化时,p 标签的文本内容会同步更新 -->
<p v-text="message"></p>
Vue 提供了一系列内置指令,涵盖内容渲染、属性绑定、事件处理、条件渲染、列表渲染等各个方面。我们逐一来看。
二、内容渲染指令
2.1 v-text
作用 :更新元素的 textContent。
html
<!-- Vue 2 & Vue 3 通用 -->
<span v-text="userName"></span>
<!-- 等价于 -->
<span>{{ userName }}</span>
当数据变化时,元素内容会同步更新。两者等价,但 v-text 会覆盖元素原有的内容。
2.2 v-html
作用 :更新元素的 innerHTML,可以渲染 HTML 字符串。
html
<template>
<div v-html="htmlContent"></div>
</template>
<script setup>
const htmlContent = '<h1>这是标题</h1><p>这是段落</p>'
</script>
⚠️ 安全警告 :在网站上动态渲染任意 HTML 是非常危险的,容易导致 XSS 攻击 。请只在可信内容上使用
v-html,永远不要在用户提交的内容上使用。
三、条件渲染指令
3.1 v-if、v-else-if、v-else
这组指令用于根据表达式的值条件性地渲染 元素或组件。v-if 是"真正"的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
html
<template>
<div v-if="status === 'loading'">加载中...</div>
<div v-else-if="status === 'success'">加载成功!</div>
<div v-else>加载失败,请重试</div>
</template>
<script setup>
import { ref } from 'vue'
const status = ref('loading')
</script>
3.2 v-show
v-show 也是条件渲染,但实现方式不同------它通过 CSS 的 display 属性来控制元素的显示与隐藏。元素始终会被渲染并保留在 DOM 中。
html
<div v-show="isVisible">这个元素始终存在,只是切换显示</div>
v-if vs v-show:如何选择?
| 对比维度 | v-if | v-show |
|---|---|---|
| 渲染方式 | 真正的条件渲染(创建/销毁 DOM) | CSS display 控制(始终保留 DOM) |
| 切换开销 | 高(销毁/重建) | 低(仅切换 CSS) |
| 初始渲染开销 | 条件为 false 时不渲染 | 无论条件如何都会渲染 |
| 适用场景 | 切换频率低的场景 | 切换频率高的场景 |
最佳实践 :如果需要非常频繁地切换,使用
v-show;如果条件在运行时很少改变,使用v-if。
四、列表渲染指令:v-for
4.1 基本用法
v-for 用于基于一个数组或对象来渲染一个列表。
遍历数组:
html
<template>
<ul>
<!-- item 是当前元素,index 是索引(可选) -->
<li v-for="(item, index) in todoList" :key="item.id">
{{ index + 1 }}. {{ item.text }}
</li>
</ul>
</template>
<script setup>
const todoList = ref([
{ id: 1, text: '学习 Vue 指令' },
{ id: 2, text: '写一篇博客' },
{ id: 3, text: '分享给朋友' }
])
</script>
遍历对象:
html
<template>
<ul>
<!-- 可以获取值、键名、索引 -->
<li v-for="(value, key, index) in userInfo" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
</template>
<script setup>
const userInfo = {
name: '张三',
age: 25,
city: '深圳'
}
</script>
遍历数字范围:
html
<!-- 渲染 1 到 10 -->
<span v-for="n in 10" :key="n">{{ n }}</span>
4.2 key 的重要性
在使用 v-for 时,务必绑定 :key 。key 用于跟踪每个节点的身份,以便 Vue 在更新列表时能够高效地重用和重新排序现有元素。
html
<!-- 推荐:使用唯一 ID -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
<!-- 不推荐:使用索引(当列表顺序变化时会引发问题) -->
<div v-for="(item, index) in items" :key="index">{{ item.name }}</div>
最佳实践 :
key的值应该是基础类型的值(string|number|symbol),并且是唯一且稳定的。
五、属性绑定指令:v-bind
v-bind 用于动态绑定一个或多个 attribute,或一个组件 prop 到表达式。
html
<!-- 基本用法 -->
<img v-bind:src="imageUrl" alt="图片">
<!-- 简写形式(推荐) -->
<img :src="imageUrl" alt="图片">
<!-- 动态绑定 class -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<!-- 动态绑定 style -->
<div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
<!-- 绑定多个属性 -->
<div v-bind="{ id: elementId, title: tooltip }"></div>
在 Vue 2 和 Vue 3 中,v-bind 的基本用法完全一致。
六、事件绑定指令:v-on
v-on 用于绑定事件监听器。简写为 @。
html
<!-- 基本用法 -->
<button v-on:click="handleClick">点击我</button>
<!-- 简写形式(推荐) -->
<button @click="handleClick">点击我</button>
<!-- 传递参数 -->
<button @click="handleDelete(item.id)">删除</button>
<!-- 访问原生事件对象 -->
<button @click="handleClick($event)">点击</button>
<!-- 事件修饰符 -->
<form @submit.prevent="handleSubmit"> <!-- 阻止默认行为 -->
<input @keyup.enter="search"> <!-- 按 Enter 键触发 -->
<button @click.stop="doThis"> <!-- 阻止事件冒泡 -->
</form>
Vue 2 → Vue 3 的变化
1. .native 修饰符被移除
在 Vue 2 中,监听组件根元素的原生事件需要使用 .native 修饰符:
html
<!-- Vue 2 -->
<MyComponent @click.native="handleClick" />
在 Vue 3 中,所有事件监听器都会被添加到组件的根元素上,不再需要 .native:
html
<!-- Vue 3 -->
<MyComponent @click="handleClick" />
2. $listeners 被移除
在 Vue 3 的虚拟 DOM 中,事件监听器现在只是以 on 为前缀的 attribute,成为了 $attrs 对象的一部分,因此 $listeners 被移除了。
3. 按键码支持被移除
Vue 2 支持使用数字键码作为 v-on 修饰符,Vue 3 不再支持。建议使用具体的按键名称,如 @keyup.enter、@keyup.page-down 等。
七、表单绑定指令:v-model
v-model 是 Vue 中最常用的指令之一,用于在表单控件与组件数据之间创建双向绑定。
7.1 基础用法
html
<template>
<input v-model="message" placeholder="输入内容">
<p>你输入的是:{{ message }}</p>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('')
</script>
v-model 本质上是一个语法糖 ------它相当于绑定了 value 属性并监听了 input 事件。
7.2 修饰符
| 修饰符 | 作用 |
|---|---|
.lazy |
在 change 事件而非 input 事件后更新 |
.number |
自动将用户输入转为数值类型 |
.trim |
自动过滤用户输入的首尾空白字符 |
html
<input v-model.lazy="content">
<input v-model.number="age">
<input v-model.trim="username">
7.3 Vue 2 → Vue 3 的重大变化
这是 Vue 2 到 Vue 3 变化最大的指令之一。
变化一:默认 prop 和事件名称改变
| Vue 2 | Vue 3 | |
|---|---|---|
| Prop | value |
modelValue |
| 事件 | input |
update:modelValue |
Vue 2 写法:
html
<!-- 父组件 -->
<ChildComponent v-model="pageTitle" />
<!-- 等价于 -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
javascript
// ChildComponent.vue - Vue 2
export default {
model: {
prop: 'value',
event: 'input'
},
props: {
value: String
}
}
Vue 3 写法:
html
<!-- 父组件 -->
<ChildComponent v-model="pageTitle" />
<!-- 等价于 -->
<ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event" />
javascript
// ChildComponent.vue - Vue 3
// 直接声明 modelValue prop
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
变化二:支持多个 v-model 绑定
这是 Vue 3 最实用的新增能力之一------可以在同一个组件上使用多个 v-model:
html
<!-- 父组件:多个 v-model -->
<UserForm
v-model:name="userName"
v-model:email="userEmail"
v-model:age="userAge"
/>
javascript
// UserForm.vue - 子组件
defineProps({
name: String,
email: String,
age: Number
})
defineEmits(['update:name', 'update:email', 'update:age'])
html
<!-- 子组件模板 -->
<input :value="name" @input="$emit('update:name', $event.target.value)">
<input :value="email" @input="$emit('update:email', $event.target.value)">
<input type="number" :value="age" @input="$emit('update:age', $event.target.value)">
变化三:.sync 修饰符被移除
在 Vue 2 中,.sync 修饰符用于实现 prop 的"双向绑定":
html
<!-- Vue 2 -->
<ChildComponent :title.sync="pageTitle" />
在 Vue 3 中,.sync 被 v-model 的参数形式取代:
html
<!-- Vue 3:等价于上面的 Vue 2 写法 -->
<ChildComponent v-model:title="pageTitle" />
八、Vue 2 与 Vue 3 核心差异:v-if 与 v-for 的优先级
这是 Vue 2 到 Vue 3 的另一个重大变化。
8.1 优先级差异
| Vue 版本 | 优先级 |
|---|---|
| Vue 2 | v-for > v-if |
| Vue 3 | v-if > v-for |
这意味着在 Vue 2 中,v-for 会先执行,然后对每个循环项执行 v-if。而在 Vue 3 中,v-if 先执行,因此无法访问 v-for 中的变量。
8.2 代码示例对比
Vue 2 可以这样写(但不推荐):
html
<!-- Vue 2:v-for 优先级高于 v-if -->
<ul>
<li v-for="item in todoList" v-if="!item.done" :key="item.id">
{{ item.task }}
</li>
</ul>
Vue 3 中这样写会报错 ,因为 v-if 先执行时访问不到 v-for 中的 item。
Vue 3 的正确写法 :将 v-for 放在 <template> 标签上:
html
<!-- Vue 3 正确写法 -->
<ul>
<template v-for="item in todoList" :key="item.id">
<li v-if="!item.done">{{ item.task }}</li>
</template>
</ul>
8.3 最佳实践:无论 Vue 2 还是 Vue 3,都不要同时使用
官方强烈建议 不要在同一元素上同时使用 v-if 和 v-for。更好的做法是使用计算属性来预处理数据:
javascript
// 使用计算属性过滤数据
const todoList = ref([
{ id: 1, task: '吃饭', done: true },
{ id: 2, task: '睡觉', done: false },
{ id: 3, task: '洗澡', done: true }
])
// 计算属性:只返回未完成的任务
const unfinishedTodos = computed(() => {
return todoList.value.filter(item => !item.done)
})
// 计算属性:只返回已完成的任务
const finishedTodos = computed(() => {
return todoList.value.filter(item => item.done)
})
html
<!-- 模板中干净地使用 v-for -->
<ul>
<li v-for="item in unfinishedTodos" :key="item.id">
{{ item.task }}
</li>
</ul>
这样做的好处是:
- 性能更好:数据提前过滤,减少了不必要的循环
- 代码更清晰:模板逻辑更简洁
- Vue 2/3 兼容:不受优先级差异影响
九、Vue 3 新增指令
Vue 3 引入了一些新指令,主要用于性能优化 和开发体验提升。
9.1 v-memo(Vue 3.2+)
v-memo 是 Vue 3.2 新增的指令,用于记忆化模板的一部分 。它接受一个依赖数组,只有在数组中的值发生变化时,才会重新渲染。
html
<template>
<!-- 只有当 msg 变化时才重新渲染 -->
<p v-memo="[msg]">{{ msg }}</p>
<!-- 传入空数组 = 永不重新渲染(等同于 v-once) -->
<p v-memo="[]">{{ staticContent }}</p>
<button @click="msg = '新消息'">更新</button>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('Hello')
const staticContent = ref('永远不会变')
</script>
适用场景:
- 大型数据表格中的某一行渲染
- 复杂组件树中不需要频繁更新的静态部分
- 需要精细控制渲染性能的场景
注意 :
v-memo在v-for循环中不起作用。
9.2 v-once(Vue 2 已有,Vue 3 延续)
v-once 指令仅渲染元素/组件一次。初始渲染后,该元素及其所有子元素将被视为静态内容。
html
<!-- 单个元素 -->
<span v-once>{{ msg }}</span>
<!-- 有子元素的元素 -->
<div v-once>
<p>{{ msg }}</p>
</div>
<!-- v-for 中使用(列表只在初次渲染时生成) -->
<ul>
<li v-for="i in list" v-once>{{ i }}</li>
</ul>
适用场景:
- 数据初始化后不再改变的内容
- 用于减少不必要的重新渲染
⚠️ 注意 :当
v-once与v-if或v-show一起使用时,条件判断只在初次渲染时生效。如果元素在初次渲染时是可见的,它将始终可见;如果隐藏,将永远隐藏。
9.3 v-pre(Vue 2 已有)
v-pre 用于跳过当前元素及其子元素的编译过程,显示原始的 Mustache 标签。
html
<!-- 正常解析:显示 "Andy" -->
<div>{{ person.name }}</div>
<!-- v-pre:显示 "{{ person.name }}" -->
<div v-pre>{{ person.name }}</div>
适用场景:
- 在文档中展示 Vue 代码示例
- 跳过没有指令和插值表达式的节点,提高编译速度
9.4 v-cloak(Vue 2 已有)
v-cloak 用于在 Vue 实例编译完成之前,隐藏未编译的 Mustache 标签。
html
<template>
<div v-cloak>{{ message }}</div>
</template>
<style>
/* 配合 CSS 使用 */
[v-cloak] {
display: none;
}
</style>
当网络较慢或页面渲染较慢时,用户可能会看到 {``{ message }} 这样的模板代码一闪而过。v-cloak 配合 CSS 的 display: none 可以解决这个问题。
十、自定义指令
除了内置指令,Vue 允许开发者注册自定义指令。
10.1 Vue 2 的自定义指令
在 Vue 2 中,自定义指令使用以下钩子函数:
javascript
// 全局注册
Vue.directive('highlight', {
// 指令绑定到元素后调用(只调用一次)
bind(el, binding, vnode) {
el.style.background = binding.value
},
// 元素插入父 DOM 后调用
inserted(el, binding, vnode) {},
// 元素更新时调用
update(el, binding, vnode, oldVnode) {},
// 组件和子级更新后调用
componentUpdated(el, binding, vnode, oldVnode) {},
// 指令移除时调用(只调用一次)
unbind(el, binding, vnode) {}
})
// 局部注册
export default {
directives: {
highlight: {
bind(el, binding) {
el.style.background = binding.value
}
}
}
}
10.2 Vue 3 的自定义指令(重大变化)
在 Vue 3 中,自定义指令的钩子函数被重命名,以更好地与组件生命周期保持一致。
钩子函数映射表:
| Vue 2 | Vue 3 |
|---|---|
bind |
beforeMount |
inserted |
mounted |
update |
❌ 已移除(使用 updated 替代) |
componentUpdated |
updated |
unbind |
unmounted |
| --- | created(新增) |
| --- | beforeUpdate(新增) |
| --- | beforeUnmount(新增) |
Vue 3 写法:
javascript
// 全局注册
const app = createApp({})
app.directive('highlight', {
// 新增:在元素的 attribute 或事件监听器被应用之前调用
created(el, binding, vnode, prevVnode) {},
// 对应 Vue 2 的 bind
beforeMount(el, binding, vnode) {
el.style.background = binding.value
},
// 对应 Vue 2 的 inserted
mounted(el, binding, vnode) {},
// 新增:在元素本身被更新之前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 对应 Vue 2 的 componentUpdated
updated(el, binding, vnode, prevVnode) {},
// 新增:在元素被卸载之前调用
beforeUnmount(el, binding, vnode) {},
// 对应 Vue 2 的 unbind
unmounted(el, binding, vnode) {}
})
// 局部注册(使用 Composition API)
<script setup>
// Vue 3.2+ 支持在 setup 中直接定义以 v 开头的变量作为自定义指令
const vHighlight = {
beforeMount(el, binding) {
el.style.background = binding.value
}
}
</script>
10.3 访问组件实例的变化
在 Vue 2 中,需要通过 vnode.context 访问组件实例:
javascript
// Vue 2
bind(el, binding, vnode) {
const vm = vnode.context
}
在 Vue 3 中,实例现在是 binding 参数的一部分:
javascript
// Vue 3
mounted(el, binding, vnode) {
const vm = binding.instance
}
10.4 实战案例:v-debounce 防抖指令
下面是一个完整的自定义指令案例,实现输入防抖功能,在 Vue 2 和 Vue 3 中分别实现。
Vue 2 实现:
javascript
// Vue 2
Vue.directive('debounce', {
bind(el, binding) {
let timer = null
const delay = binding.arg || 300 // 从指令参数获取延迟时间
el.addEventListener('input', function() {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
// 触发绑定的事件处理函数
binding.value(this.value)
}, delay)
})
}
})
html
<!-- Vue 2 使用 -->
<input v-debounce:500="handleSearch">
Vue 3 实现:
javascript
// Vue 3
const app = createApp({})
app.directive('debounce', {
beforeMount(el, binding) {
let timer = null
const delay = binding.arg || 300
el.addEventListener('input', function() {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
binding.value(this.value)
}, delay)
})
},
// 组件卸载时清理定时器
unmounted(el) {
// 清理逻辑...
}
})
html
<!-- Vue 3 使用(语法相同) -->
<input v-debounce:500="handleSearch">
十一、指令速查表
| 指令 | 简写 | 作用 | Vue 2 | Vue 3 |
|---|---|---|---|---|
v-text |
--- | 更新元素的 textContent | ✅ | ✅ |
v-html |
--- | 更新元素的 innerHTML | ✅ | ✅ |
v-if |
--- | 条件渲染(创建/销毁) | ✅ | ✅ |
v-else-if |
--- | v-if 的 else-if 块 | ✅ | ✅ |
v-else |
--- | v-if 的 else 块 | ✅ | ✅ |
v-show |
--- | 条件渲染(CSS 切换) | ✅ | ✅ |
v-for |
--- | 列表渲染 | ✅ | ✅ |
v-bind |
: |
动态绑定属性 | ✅ | ✅ |
v-on |
@ |
绑定事件监听 | ✅ | ✅ |
v-model |
--- | 双向数据绑定 | ✅ | 变化较大 |
v-slot |
# |
插槽内容分发 | ✅ | ✅ |
v-pre |
--- | 跳过编译 | ✅ | ✅ |
v-cloak |
--- | 隐藏未编译模板 | ✅ | ✅ |
v-once |
--- | 仅渲染一次 | ✅ | ✅ |
v-memo |
--- | 记忆化模板子树 | ❌ | Vue 3.2+ 新增 |
.sync 修饰符 |
--- | 双向绑定 prop | ✅ | 已移除 |
.native 修饰符 |
--- | 监听组件根元素原生事件 | ✅ | 已移除 |
十二、总结
通过本文的梳理,我们可以看到 Vue 从 2 到 3 在指令层面的演进思路:
| 演进方向 | 具体体现 |
|---|---|
| 更合理的优先级 | v-if 与 v-for 的优先级调整 |
| 更统一的 API | v-model 的标准化、自定义指令钩子与组件生命周期对齐 |
| 更灵活的能力 | 多个 v-model 绑定、v-model 参数化 |
| 更精细的性能控制 | v-memo 指令的引入 |
对于日常开发来说,以下变化最值得关注:
v-model写法变了 :默认 prop 从value变为modelValue,事件从input变为update:modelValue.sync不用了 :用v-model:propName替代v-if和v-for不能同时用了:改用计算属性过滤数据- 自定义指令钩子改名了 :
bind→beforeMount,inserted→mounted等 .native修饰符没了:Vue 3 会自动将事件监听添加到根元素
理解这些变化,不仅能帮你顺利从 Vue 2 迁移到 Vue 3,更能让你写出更优雅、更高效的 Vue 代码。
下一篇博客,我们聊聊 Vue 3 的组合式 API(Composition API) ------从 Options API 到 Composition API 的演进,以及如何在真实项目中用好它。