Vue 的模板语法、指令和插值是构建 Vue 应用的核心,它们为开发者提供了便捷的方式来实现数据与视图的绑定,以及丰富多样的交互效果。
一、插值:数据绑定的基础
1. 什么是插值?
插值( Interpolation )是 Vue 模板语法中用于将数据动态嵌入到 HTML 结构中的一种方式。
它允许开发者在模板中直接引用 Vue 实例中的数据,并实时展示其值。
简单来说,插值就像是在 HTML 页面里预留一个 "占位符",Vue 会用实际的数据替换这个占位符,从而实现数据与视图的绑定。
2. 文本插值 {{ }}
文本插值使用双大括号 {{ }}
来嵌入数据,它不仅能显示简单的字符串,还支持简单的 JavaScript 表达式运算。
html
<template>
<div>
<!-- 显示 message 变量的值,最终会显示:Hello, Vue! -->
<p>{{ message }}</p>
<!-- 计算表达式 2 + 3 的值并显示,结果为:5 -->
<p>{{ 2 + 3 }}</p>
<!-- 根据 isTrue 的值,条件为真显示 Yes,否则显示 No,当前显示:Yes -->
<p>{{ isTrue? 'Yes' : 'No' }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 定义响应式变量 message,值为 'Hello, Vue!'
const message = ref('Hello, Vue!');
// 定义响应式变量 isTrue,值为 true
const isTrue = ref(true);
</script>
不过,在双大括号中只能使用简单的表达式,不能使用语句,如 if
语句、for
循环等。这是因为插值表达式的目的是简洁地计算并显示数据,而不是执行复杂的逻辑。
自动转义机制:Vue 会自动对插值内容进行转义,以防止 XSS 攻击。
XSS 攻击,即跨站脚本攻击(Cross-Site Scripting),是一种常见的 web 安全漏洞。攻击者通过注入恶意的脚本代码(如 JavaScript)到网页中,当用户访问该网页时,这些恶意脚本就会在用户的浏览器中执行,从而可能窃取用户的敏感信息(如 cookie、登录凭证等)、篡改页面内容或进行其他恶意操作。
例如,当我们插入包含 HTML 标签的字符串时,这些标签会被转义为 HTML 实体。
html
<template>
<div>
<!-- 插值内容会自动转义 -->
<p>{{ '<div>Hello</div>' }}</p>
</div>
</template>
上述代码会输出 <div>Hello</div>
,而不是渲染成一个 div
元素。
- 这与
v-html
指令形成鲜明对比,v-html
会直接渲染 HTML 代码,使用时需要格外注意安全问题。
3. 原始 HTML 插值 v-html
当我们需要将数据以 HTML 的形式渲染到页面上时,可以使用 v-html
指令。
html
<template>
<div>
<!-- 使用 v-html 渲染 HTML 内容 -->
<div v-html="rawHtml"></div>
</div>
</template>
<script>
export default {
data() {
return {
rawHtml: '<span style="color: red">This is red text</span>'
};
}
};
</script>
⚠️ v-html
存在安全风险,它会直接渲染传入的 HTML 代码。如果这些代码来自用户输入或者不可信的来源,可能会导致XSS攻击。
二、指令:动态交互的核心
1. 什么是指令
指令(Directives)是一种特殊的 HTML 属性( Attribute ),用于为 HTML 元素添加特殊的逻辑或行为。它以 v-
为前缀(部分有简写形式)。
普通 HTML 属性(如 class
、id
、href
等)主要用于定义元素的静态特性,其值通常是静态的,直接由浏览器进行渲染。
与普通的 HTML 属性不同,指令本质上是 Vue 对 DOM 操作的封装,通过元素标签上声明指令,开发者无需直接操作 DOM,而是通过声明式语法控制元素的渲染方式、响应事件、绑定数据等,从而实现数据驱动视图的动态更新。
2. 属性绑定 v-bind
动态绑定 class/style :v-bind
指令用于将 Vue 实例中的数据绑定到 HTML 元素的属性上。常见的应用场景包括动态绑定 class
和 style
属性。
v-bind
可以简写为:
,这是一种更简洁的写法。
html
<template>
<div>
<!-- 动态绑定 class 属性 -->
<p :class="{ active: isActive, 'text-bold': isBold }">This is a paragraph</p>
<!-- 动态绑定 style 属性 -->
<p :style="{ color: textColor, fontSize: fontSize + 'px' }">This is another paragraph</p>
</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
isBold: false,
textColor: 'blue',
fontSize: 16
};
}
};
</script>
上述代码中,class
属性使用对象语法动态切换类名,style
属性使用对象语法动态设置样式。
3. 条件渲染
v-if
vs v-show
v-if
和 v-show
都用于根据条件显示或隐藏元素,但它们的实现方式和性能特点有所不同。
指令 | 实现方式 | 性能特点 | 适用场景 |
---|---|---|---|
v-if | 惰性渲染:条件为假时不渲染或移除 DOM 元素,为真时插入 DOM | 条件少变时性能优,频繁操作 DOM 开销大 | 条件不常变场景 |
v-show | 始终渲染,通过切换元素的 display 属性控制显隐 |
频繁切换性能优,因为不涉及 DOM 增删 | 条件频繁变场景 |
html
<template>
<div>
<!-- 使用 v-if 进行条件渲染 -->
<p v-if="score >= 90">优秀</p>
<p v-else-if="score >= 60">及格</p>
<p v-else>不及格</p>
<!-- 使用 v-show 进行条件渲染 -->
<p v-show="isVisible">This is visible</p>
</div>
</template>
<script>
export default {
data() {
return {
score: 80,
isVisible: true
};
}
};
</script>
v-else-if
和 v-else
的使用
v-else-if
和 v-else
是 v-if
的补充指令,用于实现多条件判断。它们必须紧跟在同级的 v-if
或 v-else-if
之后,且所有分支必须共享同一个父元素,否则无效。
<template>
上的 v-if
有时候,我们可能需要在不止一个元素上进行条件渲染。如果直接在多个元素上分别使用 v-if
会显得很繁琐,也会影响代码的可读性和维护性。这时,我们可以使用 <template>
标签来包裹这些元素,并在 <template>
上使用 v-if
指令。
<template>
标签是一个虚拟元素,它不会被渲染到 DOM 中,只是作为一个容器来组织多个元素。
html
<template>
<div>
<template v-if="showGroup">
<p>这是第一行文本</p>
<p>这是第二行文本</p>
<p>这是第三行文本</p>
</template>
<p v-else>条件不满足,不显示上面的文本组</p>
</div>
</template>
<script>
export default {
data() {
return {
showGroup: false
};
}
};
</script>
- 当
showGroup
为true
时,<template>
内的三个<p>
元素会被渲染到 DOM 中。 - 当
showGroup
为false
时,只会渲染v-else
对应的<p>
元素。
注意: v-show
指令不能用在 <template>
标签上,它只能应用于具体的 HTML 元素上。
- 因为
v-show
是通过修改元素的display
属性来控制显隐的,而<template>
本身不会被渲染到 DOM 中,没有display
属性可供修改。
4. 列表渲染 v-for
遍历数组 / 对象 / 数字
v-for
指令用于循环渲染一组数据,它可以遍历数组、对象和数字。
html
<template>
<div>
<!-- 遍历数组 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">{{ index + 1 }}. {{ item.name }}</li>
</ul>
<!-- 遍历对象 -->
<ul>
<li v-for="(value, key, index) in user" :key="key">{{ index + 1 }}. {{ key }}: {{ value }}</li>
</ul>
<!-- 遍历数字 -->
<ul>
<li v-for="n in 5" :key="n">{{ n }}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
],
user: {
name: 'John',
age: 30,
gender: 'male'
}
};
}
};
</script>
key
的重要性
key
属性是 v-for
指令中极为关键 的一部分,它用于帮助 Vue 识别每个节点的唯一身份,进而优化虚拟 DOM(Virtual DOM)的更新效率。
- Vue 的虚拟 DOM 采用 Diff 算法来比较新旧节点的差异,当数据发生变化时,通过
key
可以更高效地识别哪些节点需要更新,哪些节点可以直接复用,从而减少不必要的 DOM 操作,提高渲染性能。
因此,在使用 v-for
时,一定要为每个元素提供一个唯一的 key
,通常使用数据的唯一标识符作为 key
,如 id
。
为什么不建议用 index 作为 key ?
1. 数据顺序变化时导致渲染异常
当列表数据的顺序发生变化时( 比如进行插入、删除等操作 ),由于 index
是根据元素在数组中的位置动态生成的,一旦数组顺序改变,元素对应的 index
也会改变。这会使得 Vue 认为是新的元素插入或旧的元素删除,从而导致不必要的 DOM 操作,甚至可能出现渲染异常。
javascript
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
使用 index
作为 key
进行渲染:
html
<li v-for="(user, index) in users" :key="index">{{ user.name }}</li>
如果在列表开头插入一个新用户:
js
users.value.unshift({ id: 4, name: 'David' });
此时,原本 index
为 0 的 Alice
变成了 index
为 1,Bob
变成了 index
为 2,Charlie
变成了 index
为 3。即使有些元素的内容并没有改变, Vue 也会认为原来的元素都发生了变化,从而重新渲染整个列表。同理,当更新 Bob
的信息时:
js
users.value[1].name = 'Bobby';
由于使用 index
作为 key
,Vue 可能无法直接定位到 Bob
对应的节点进行更新,而是重新比较整个列表,导致性能下降。
2. 列表项存在状态绑定问题
列表项包含表单、动画等状态时,index
变化会导致状态跟随错误的节点。因为当数据顺序改变时,index
也会改变,这会使得原本绑定在某个元素上的状态被错误地应用到其他元素上。
html
<li v-for="(user, index) in users" :key="index">
{{ user.name }}
<input v-model="user.inputValue"> <!-- 排序后输入值可能错位 -->
</li>
为什么不建议同时使用 v-if
和 v-for
?
在 Vue 3 里,v-if
优先级高于 v-for
。模板编译时 v-if
先判断,这就使得 v-if
条件无法访问 v-for
作用域内定义的变量别名,若强行使用会引发错误。
而且,即便不考虑变量访问问题,二者同时使用时,会先构建完整的虚拟 DOM 结构,再用 v-if
过滤部分节点,这会带来额外的性能开销,数据量越大,性能问题越明显。
html
<template>
<!-- 这里 v-if 尝试访问未通过 `v-for` 创建的 `item` 变量,会引发错误 -->
<ul>
<li v-for="item in items" v-if="item.isVisible" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue';
const items = ref([{ id: 1, isVisible: true }]);
</script>
方法一:使用计算属性
可借助计算属性先对数据进行过滤,再用 v-for
遍历过滤后的数据。这样就能避免 v-if
和 v-for
同时出现在同一元素上。
html
<template>
<div>
<!-- 遍历过滤后的数组 -->
<ul>
<li v-for="(item, index) in filteredItems" :key="item.id">{{ index + 1 }}. {{ item.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const items = ref([
{ id: 1, name: 'Item 1', isVisible: true },
{ id: 2, name: 'Item 2', isVisible: false },
{ id: 3, name: 'Item 3', isVisible: true }
]);
const filteredItems = computed(() => {
return items.value.filter(item => item.isVisible);
});
</script>
方法二:使用 <template>
标签
把 v-if
放到 <template>
标签上,让 v-if
在 v-for
外层进行判断,以此避免不必要的遍历。
html
<template>
<div>
<!-- 外层 template 使用 v-for 遍历数字数组 -->
<template v-for="number in numbers" :key="number">
<!-- 内层 li 使用 v-if 判断是否为奇数 -->
<li v-if="number % 2 === 1">{{ number }}</li>
</template>
</div>
</template>
<script setup>
import { ref } from 'vue';
const numbers = ref([1, 2, 3, 4, 5, 6, 7, 8, 9]);
</script>
5. 事件绑定 v-on
v-on
指令用于在 DOM 元素上绑定事件监听器。通过 v-on
,开发者能够监听如点击、键盘输入、鼠标移动等各类 DOM 事件,并在事件触发时执行对应的 JavaScript 方法。
v-on
可以简写为@
,例如v-on:click
可以简写为@click
。
基本用法
html
<template>
<div>
<!-- 绑定点击事件 -->
<button @click="handleClick">点击我</button>
<!-- 绑定键盘事件(监听回车键按下) -->
<input type="text" @keyup.enter="handleEnter">
<!-- 绑定鼠标移入移出事件 -->
<div @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
鼠标移到这里试试
</div>
</div>
</template>
<script setup>
const handleClick = () => {
console.log('按钮被点击了');
};
const handleEnter = () => {
console.log('回车键被按下');
};
const handleMouseEnter = () => {
console.log('鼠标移入');
};
const handleMouseLeave = () => {
console.log('鼠标移出');
};
</script>
传递参数
- 默认传参
在使用 v-on
绑定事件时,如果没有显式地传递参数,事件处理函数会默认接收到原生事件对象 $event
。
借助这个对象,你可以访问到与该事件相关的各种信息,例如事件类型、触发事件的 DOM 元素等。
html
<template>
<!-- 绑定点击事件,不传递参数,默认传递原生事件对象 -->
<button @click="handleDefaultClick">默认传参点击</button>
</template>
<script setup>
// 定义事件处理函数,接收默认的原生事件对象
const handleDefaultClick = (event) => {
// 打印事件类型,例如 'click'
console.log('事件类型:', event.type);
// 打印触发事件的 DOM 元素
console.log('触发事件的 DOM 元素:', event.target);
};
</script>
-
传递自定义参数
在事件处理方法中,可以通过
$event
访问原生事件对象,同时也能传递自定义参数:
html
<template>
<!-- 绑定点击事件,传递自定义参数和原生事件对象 -->
<button @click="handleClick('自定义参数', $event)">点击传参</button>
</template>
<script setup>
// 定义事件处理函数
const handleClick = (arg, event) => {
// 打印自定义参数。控制台输出:自定义参数。
console.log(arg);
// 打印触发事件的 DOM 元素。控制台输出按钮对应的 DOM 元素对象信息。
console.log(event.target);
};
</script>
-
内联语句传参
也可以直接在
v-on
中使用内联 JavaScript 表达式:
html
<template>
<!-- 绑定点击事件,使用内联 JavaScript 表达式使 count 自增 -->
<button @click="count += 1">点击自增(内联)</button>
<!-- 显示当前 count 的值 -->
<p>当前计数: {{ count }}</p>
</template>
<script setup>
import { ref } from 'vue';
// 使用 ref 创建响应式数据
const count = ref(0);
</script>
事件修饰符
Vue 提供了一系列事件修饰符,用于简化常见的事件处理逻辑:
修饰符 | 作用 | 示例 |
---|---|---|
.stop |
阻止事件冒泡(等价于 event.stopPropagation() ) |
<button @click.stop="handleClick"> |
.prevent |
阻止默认事件(如表单提交、链接跳转) | <a @click.prevent="handleClick"> |
.capture |
使用捕获模式触发事件 | <div @click.capture="handleClick"> |
.self |
只有事件源自当前元素时才触发 | <div @click.self="handleClick"> |
.once |
事件仅触发一次 | <button @click.once="handleClick"> |
.passive |
提升滚动性能(用于 scroll 事件) |
<div @scroll.passive="handleScroll"> |
按键修饰符
针对键盘事件,Vue 提供了按键修饰符,用于监听特定按键:
修饰符 | 对应按键 | 示例 |
---|---|---|
.enter |
回车键 | <input @keyup.enter="handleEnter"> |
.tab |
Tab 键 | <input @keydown.tab="handleTab"> |
.delete |
Delete 或 Backspace | <input @keyup.delete="handleDelete"> |
.esc |
Esc 键 | <input @keyup.esc="handleEsc"> |
.space |
空格键 | <input @keyup.space="handleSpace"> |
.up |
向上箭头 | <input @keyup.up="handleUp"> |
.down |
向下箭头 | <input @keyup.down="handleDown"> |
系统修饰键
结合 Ctrl
、Alt
、Shift
、Meta
(Windows/Command 键)等修饰键使用:
修饰符 | 对应键 | 组合示例 | 作用 |
---|---|---|---|
.ctrl |
Ctrl |
<input @keyup.ctrl.enter="submitForm"> |
监听 Ctrl + Enter 组合键 |
.alt |
Alt 或Option (⌥) |
<button @click.alt="openMenu"> |
监听 Alt 键按下并点击 |
.shift |
Shift |
<input @keydown.shift="enableCaps"> |
监听 Shift 键按下(如大写锁定) |
.meta |
Win (⊞)或Command (⌘) |
<input @keyup.meta.c="copy"> |
监听 Win + C 组合键(系统快捷键) |
6. 表单输入绑定 v-model
v-model
是 Vue 提供的语法糖,用于在表单元素(如输入框)和组件响应式数据间创建双向绑定,让数据在组件与表单间双向同步。当用户输入文本或切换选项时,数据会自动更新;数据变化时,也能立刻反映到表单展示上。
v-model
本质上是:value
绑定属性和@input
事件的组合。
html
<!-- 手动绑定 :value 和 @input -->
<input
:value="inputText"
@input="(e) => inputText = e.target.value">
<!-- 使用 v-model 指令 -->
<input v-model="inputText">
如何使用 v-model
- 基础用法 - 文本输入框
html
<input v-model="message" type="text">
<p>输入内容: {{ message }}</p>
说明:v-model
绑定到 message
响应式变量,用户输入时 message
自动更新,反之修改 message
也会更新输入框内容。
- 单选框绑定
html
<input type="radio" id="option1" value="option1" v-model="selectedOption">
<label for="option1">选项 1</label>
<input type="radio" id="option2" value="option2" v-model="selectedOption">
<label for="option2">选项 2</label>
<p>选中值: {{ selectedOption }}</p>
说明:多个单选框绑定同一 v-model
,选中时 selectedOption
更新为对应 value
。
- 复选框绑定
- 单个复选框(布尔值)
html
<input type="checkbox" v-model="isChecked">
<p>是否选中: {{ isChecked }}</p>
isChecked
为布尔值,勾选时为 true
,否则为 false
。
- 多个复选框(数组)
html
<input type="checkbox" value="apple" v-model="selectedFruits">苹果
<input type="checkbox" value="banana" v-model="selectedFruits">香蕉
<input type="checkbox" value="cherry" v-model="selectedFruits">樱桃
<p>选中水果: {{ selectedFruits }}</p>
js
const selectedFruits = ref([])
selectedFruits
为数组,勾选的 value
会添加到数组中,取消则移除。
- 下拉框绑定
html
<select v-model="selectedColor">
<option value="red">红色</option>
<option value="green">绿色</option>
<option value="blue">蓝色</option>
</select>
<p>选中颜色: {{ selectedColor }}</p>
说明:v-model
绑定的 selectedColor
会与选中的 option
的 value
同步。
修饰符
v-model
提供了多个修饰符,用于处理常见的输入场景:
修饰符 | 作用 | 示例 |
---|---|---|
.trim |
自动过滤输入首尾空格 | <input v-model.trim="message"> |
.number |
将输入值转为数字(类型转换失败则保留原值) | <input v-model.number="age"> |
.lazy |
延迟更新(监听 change 事件,而不是默认的 input 事件) |
<input v-model.lazy="content"> |