Vue指令系统
# Vue指令系统
# 介绍
Vue的指令系统是框架提供的一套以 v- 为前缀的特殊属性,用于在模板编译阶段把声明式的标记转换为响应式的DOM操作。指令本质上是语法糖,编译后会生成对应的JavaScript代码,从而在数据变化时自动更新页面。
任何Html标签都可以使用Vue指令。
# 内容渲染指令
# v-text
等价于element.textContent,把表达式的值作为纯文本插入到元素中,不会解析HTML。
注意:该指令会覆盖元素内部的所有子节点。
<!-- 和插值语法一样可以传入变量、数组、对象、JS表达式、函数、方法等 -->
<h1 v-text="JS表达式"></h1>
<!-- 如果想直接传入文本需要使用单引号包裹内容 -->
<h1 v-text="'Hello'"></h1>
2
3
4
5
# v-html
等价于element.innerHTML,把表达式的值作为HTML插入到元素中,会解析标签。
注意:该指令会覆盖元素内部的所有子节点。
<h1 v-html="JS表达式"></h1>
# 条件渲染指令
# v-show
通过切换元素的display样式实现显示/隐藏,元素始终保留在DOM中。
频繁切换可见性时比
v-if更省性能。
<div v-show="JS表达式">内容</div>
# v-if
根据表达式的true / false来决定是否渲染该的元素。为真时渲染,在DOM中插入该元素。为假时不渲染,从DOM中移除该元素。
<div v-if="JS表达式">内容</div>
# v-else-if
为 v-if 添加多分支条件,必须紧跟在 v-if 或另一个 v-else-if 之后。
<div v-if="JS表达式1">内容1</div>
<div v-else-if="JS表达式2">内容2</div>
2
# v-else
为 v-if 添加否则分支条件,必须紧跟在 v-if 或 v-else-if 之后。
<div v-if="JS表达式1">内容1</div>
<div v-else-if="JS表达式2">内容2</div>
<div v-else>内容3</div>
2
3
# 属性/事件绑定指令
# v-bind
把JS表达式绑定到属性、class、style等。
它还支持使用各类修饰符,例如:.prop(绑定为DOM属性)、.camel(驼峰转 kebab-case)、.sync(实现双向绑定) 等。
<!--
格式:<标签名 v-bind:属性名="JS表达式"/>
简写:<标签名 :属性名="JS表达式"/>
-->
<img :src="img_path"/>
<div :style="div_style"/>
<div :class="div_class"/>
2
3
4
5
6
7
8
对于class、style这类值可能多个的属性,除了字符串还可以使用数组、对象,以方便追加、更改、删除其中元素。
# v-on
把JS表达式绑定到事件,也就是为元素添加事件监听,事件被触发则执行JS表达式。
它还支持使用各类修饰符,例如 .stop(阻止冒泡)、.prevent(阻止默认)、.once(只触发一次) 等。
格式:
<标签名 v-on:事件名="JS表达式"/>@简写:
<标签名 @事件名="JS表达式"/>使用修饰符:
<标签名 @事件名.修饰符="JS表达式"/>
<button @click="handleClick()"></button>
<input @keyup.enter="handleInput()"/>
2
传参时,如果不指定"()"则会将事件对象作为第一个参数,传入给函数。
如果指定了"()"但还是想传入事件对象,则可以手动传入$event对象,例如:handleClick(123, $event)。
# 常用事件
| 事件 | 触发时机 | 常用修饰符 / 备注 |
|---|---|---|
click | 鼠标左键在元素上 按下并释放 | .stop(阻止冒泡)、.prevent(阻止默认) |
dblclick | 鼠标左键 双击 | 常用于编辑、快速打开 |
mousedown | 鼠标左键 按下(未释放) | 与 mouseup 配合实现拖拽 |
mouseup | 鼠标左键 释放 | 与 mousedown 配合 |
mouseenter | 鼠标指针 进入 元素(不冒泡) | 常配合 .once 只触发一次 |
mouseleave | 鼠标指针 离开 元素(不冒泡) | 同上 |
mousemove | 鼠标在元素内部 移动(持续触发) | 用于绘图、拖拽实时反馈 |
mouseover | 鼠标指针 进入 元素(会冒泡) | 与 mouseenter 区别在于冒泡 |
mouseout | 鼠标指针 离开 元素(会冒泡) | 与 mouseleave 区别在于冒泡 |
focus | 元素(如 <input>)获得焦点 | .prevent 常用于阻止默认聚焦行为 |
blur | 元素 失去焦点 | 常配合表单校验 |
input | 表单控件 内容变化(实时) | 适用于 <input>、<textarea>,配合 v-model |
change | 表单控件 值改变后失去焦点(或选项切换) | 常用于 <select>、复选框 |
keydown | 键盘 按下(会持续触发) | .enter、.esc 等键位修饰 |
keyup | 键盘 释放 | 常用于提交、搜索等 |
reset | <form> 重置(点击 reset 按钮) | 可配合 @reset.prevent 阻止默认 |
submit | <form> 提交(点击 submit 按钮或回车) | .prevent 防止页面刷新 |
scroll | 元素或窗口 滚动 | 常用于懒加载、滚动监听 |
dragstart / dragover / drop | 拖拽操作的不同阶段 | 需要 event.preventDefault() 才能正常放置 |
contextmenu | 右键 打开上下文菜单 | 可配合 .prevent 自定义右键菜单 |
# 常用事件修饰
基础事件修饰符
| 修饰符 | 作用说明 |
|---|---|
.stop | 阻止事件冒泡,子元素的事件不会向父元素传播 |
.prevent | 阻止默认行为(如 <a> 链接跳转、表单提交) |
.self | 仅当事件目标是绑定元素本身时才触发,忽略子元素冒泡 |
.once | 事件只会触发一次,随后自动移除监听 |
键盘按键修饰符
| 修饰符 | 说明 |
|---|---|
.enter、.tab、.space、.esc、.delete、.backspace | 仅在对应键被按下时触发 |
.up、.down、.left、.right | 方向键 |
.ctrl、.shift、.alt、.meta | 系统修饰键,可组合使用(如 .ctrl.enter) |
.exact | 只在精确匹配指定修饰键时触发,多按其他按键则不触发,Vue 3中可用 |
.[KeyCode] | KeyCode对应的按键被按下时触发 |
鼠标按钮修饰符
| 修饰符 | 说明 |
|---|---|
.left、.middle、.right | 指定鼠标左键/中键/右键点击 |
可与系统修饰键组合(如 .right.shift)实现更细粒度控制 |
# 双向绑定指令
# v-model
在表单控件(input、textarea、select等)上实现双向数据绑定,即实现Value与响应式变量的数据同步。
格式:
<标签名 v-model="响应式变量名"/>
<!-- 绑定到username响应式变量 -->
<!-- 绑定之后,输入框中输入的内容,会实时同步到username变量中 -->
<input type="text" v-model="username">
2
3
对原生表单元素:
v-model绑定value。对自定义组件:
v-model默认绑定modelValue,所以在使用如Naive-ui等自定义的组件时,需要使用v-model:value才能绑定value。
# 列表渲染指令
# v-for
遍历数组、对象、字符串或数值,以循环生成标签或组件。
<!--
格式:<标签名 v-for="元素名 for 数组/对象/字符串/数字"/>
引用:{{元素名}}、{{元素名.属性名}}
-->
<!-- 循环对象 -->
<!-- goods_list变量为[{id:xxx, name:xxx, price:xxx, count:xxx},...] -->
<tr v-for="item in goods_list">
<!-- 取出对象 -->
<th scope="row">{{item.id}}</th>
<td>{{item.name}}</td>
<td>{{item.price}}</td>
<td>{{item.count}}</td>
</tr>
<!-- 循环字符串 -->
<!-- 会将字符串的每个字符作为元素进行迭代 -->
<span v-for="item in 'Hello world!'">{{item}}</span>
<!-- 循环数字 -->
<!-- 会生成值从1开始到指定数值的数组进行迭代 -->
<span v-for="item in 10">{{item}}</span>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 虚拟DOM标识
建议添加Key来提高DOM的更新数据,Vue中使用的是虚拟DOM,一但更新数据,就会生成新的虚拟DOM,然后和原生DOM进行比较,如果有两者差异则更新到原生DOM。有Key时Vue能精准定位并最小化更新,没有Key时只能靠位置匹配,可能导致大量不必要的DOM重建。
如果我们的列表渲染时添加了唯一Key值,则Vue在更新原生DOM时,只需要差异更新列表中发生改变的数据即可。
如果我们的列表渲染时不使用唯一Key值,则Vue无法判断我们的数据是在哪里进行更改,更新时可能需要将列表的虚拟DOM完全替换原生DOM,速度相较差异更新会慢很多。
格式:
:key="唯一值"Key只要在同级节点之间唯一即可,所以列表渲染时只需要每个直接子节点的Key不同即可。
另外绑定的Key只在Vue的虚拟DOM中存在,不会出现在浏览器的原生DOM里。
<!--
此处是字符串拼接对象元素的ID作为唯一Key值
如果数据中的ID不是唯一,也可以加上索引作为补充
-->
<tr v-for="(item, index) in goods_list" :key="index+'_'+item.id">
<th scope="row">{{item.id}}</th>
<td>{{item.name}}</td>
<td>{{item.price}}</td>
<td>{{item.count}}</td>
</tr>
2
3
4
5
6
7
8
9
10
# 使用例子
# 内容渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue测试页面</title>
<script src="./vue.js"></script>
</head>
<body>
<div id="app">
<h1 v-text="name"><p>我会被覆盖</p></h1>
<h1 v-text="hello()"></h1>
<h1 v-html="links"></h1>
</div>
</body>
<script>
const {createApp, ref} = Vue;
createApp({
setup() {
const name = ref('hello');
return {
name,
hello() {
return "what?"
},
links: '<a href="https://www.baidu.com/">百度一下</a>'
}
}
}).mount("#app")
</script>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 条件渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue测试页面</title>
<script src="./vue.js"></script>
</head>
<body>
<div id="app">
<h1 v-show="is_show_name">名称: {{name}}</h1>
<h1 v-if="is_show_age">年龄: {{age}}</h1>
<h1>你属于:
<span v-if="age>=60">老年人</span>
<span v-else-if="age>=18">成年人</span>
<span v-else>你就是个Baby!</span>
</h1>
</div>
</body>
<script>
const {createApp, ref} = Vue;
createApp({
setup() {
const name = ref('John');
const age = ref(15);
const is_show_name = ref(true);
const is_if_age = ref(false);
return {
name,
age,
is_show_name,
is_if_age
}
}
}).mount("#app")
</script>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 属性/事件绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue测试页面</title>
<script src="./vue.js"></script>
<style>
.my_color {
background-color: bisque;
}
.my_area {
height: 50px;
width: 100px;
}
</style>
</head>
<body>
<div id="app">
<div :style="div_style"></div>
<br>
<button @click="handleChangeDivStyleColor('red')">改变颜色</button>
<br><br>
<div :class="div_class"></div>
<br><br>
<button @click="click_times++">点击次数: {{click_times}}</button>
<br><br>
<button @click="handleToogleRotateImg()">点击我开关图片轮换</button>
<br><br>
<div><img :src="img_path" height="200px" width="200px"/></div>
</div>
<script>
const {createApp, ref} = Vue;
createApp({
setup() {
function getRandImgPath() {
// 确保不会随机到上一张图片
let rand_path = img_path.value;
while (img_path.value === rand_path) {
rand_path = "./img/" + (Math.floor(Math.random() * 5) + 1).toString() + ".png"
}
return rand_path
}
function handleToogleRotateImg() {
let _this = this
if (this.t) {
clearInterval(this.t)
this.t = null
} else {
_this.t = setInterval(function () {
img_path.value = getRandImgPath()
}, 1000)
}
}
const div_style = ref({
"width": "100px",
"height": "100px",
"backgroundColor": "aquamarine"
});
const div_class = ref(["my_color", "my_area"]);
const click_times = ref(0);
const img_path = ref();
// 获取默认图片
img_path.value = getRandImgPath();
function handleChangeDivStyleColor(color){
div_style.value.backgroundColor = color;
}
return {
div_style,
handleChangeDivStyleColor,
div_class,
click_times,
img_path,
handleToogleRotateImg
}
}
}).mount("#app")
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue测试页面</title>
<script src="./vue.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="username">
<div>你输入了: {{username}}</div>
</div>
<script>
const {createApp, ref} = Vue;
createApp({
setup() {
const username = ref();
return {
username
}
}
}).mount("#app")
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 列表渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue测试页面</title>
<script src="./vue.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<div id="app">
<div class="container-fluid">
<div class="row">
<div class="col-md-6 offset-md-3">
<h1 class="text-center">购物车</h1>
<div class="text-center">
<button class="btn btn-success" @click="handleLoad">加载购物车</button>
</div>
<br>
<div class="text-center" v-if="goods_list.length<=0">购物车是空的!</div>
<table class="table table-striped table-bordered text-center" v-if="goods_list.length>0">
<thead>
<tr>
<th scope="col"><input type="checkbox" v-model="checked_all" @change="handleCheckAll"></th>
<th scope="col">商品ID</th>
<th scope="col">商品名称</th>
<th scope="col">商品价格</th>
<th scope="col">商品数量</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in goods_list" :key="index+'_'+item.id">
<td><input type="checkbox" v-model="checked_goods_list" :value="item" @change="handleCheckCase">
</td>
<th scope="row">{{item.id}}</th>
<td>{{item.name}}</td>
<td>{{item.price}}</td>
<td class="w-25">
<div class="input-group input-group-sm">
<button class="btn btn-secondary" type="button" @click="handleItemSub(item)">-</button>
<input type="text" class="form-control text-center" v-model="item.count">
<button class="btn btn-success" type="button" @click="item.count++">+</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="text-lg-end" v-if="goods_list.length>0">
总价格:{{getTotalPrice()}}
</div>
</div>
</div>
</div>
</div>
<script>
const {createApp, ref} = Vue;
createApp({
setup() {
function handleLoad() {
goods_list.value = [
{id: 1, name: "手柄", price: 98, count: 700},
{id: 2, name: "水杯", price: 49, count: 50},
{id: 3, name: "咖啡", price: 24, count: 500},
{id: 4, name: "雨伞", price: 29, count: 100}
];
}
function getTotalPrice() {
let tmp = 0;
for (let i in checked_goods_list.value) {
let item_data = checked_goods_list.value[i]
tmp += item_data.price * item_data.count
}
return tmp
}
function handleCheckAll() {
if (checked_all.value) {
checked_goods_list.value = goods_list.value
} else {
checked_goods_list.value = []
}
}
function handleCheckCase() {
if (checked_goods_list.value.length === goods_list.value.length) {
checked_all.value = true;
} else {
checked_all.value = false;
}
}
function handleItemSub(item) {
if ((item.count - 1) > 0) {
console.log(item.count--);
}
}
const goods_list = ref([]);
const checked_goods_list = ref([]);
const checked_all = ref(false);
return {
goods_list,
checked_goods_list,
checked_all,
handleLoad,
getTotalPrice,
handleCheckAll,
handleCheckCase,
handleItemSub
}
}
}).mount("#app")
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# 过滤框案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue测试页面</title>
<script src="./vue.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<div id="app">
<div class="container-fluid">
<div class="row">
<div class="col-md-6 offset-md-3 mt-3 text-center">
<h1>数据过滤</h1>
<input type="text" class="form-control w-50 mt-3 offset-md-3" v-model="filter_text"
@input="handleFilterData">
<table class="table table-bordered table-hover w-50 mt-3 offset-md-3">
<tbody>
<tr v-if="filtered_data.length>0" v-for="(item, index) in filtered_data">
<td>{{item}}</td>
</tr>
<tr v-else-if="filter_text.length>0">
<td>查询的数据为空!</td>
</tr>
<tr v-else v-for="(item, index) in content_data">
<td>{{item}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const {createApp, ref} = Vue;
createApp({
setup() {
const filter_text = ref("");
const content_data = ref(["Hello", "World", "WOW!", "ABCDEFG", "我来了!", "你好世界!", "我是谁,我在哪?"]);
const filtered_data = ref([]);
function handleFilterData() {
// 数组的filter方法,如果过滤函数返回true则item会保留,返回false则不保留
filtered_data.value = content_data.value.filter(function (item) {
if (item.toLowerCase().indexOf(filter_text.value.toLowerCase()) >= 0) {
return true
}
return false
})
}
return {
filter_text,
content_data,
filtered_data,
handleFilterData
}
}
}).mount("#app")
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63