Vue笔记-系列
Vue笔记[一]-初识
Vue笔记[二]-组件化
Vue笔记[三]-ToDoList
Vue笔记[四]-动画、Vuex
Vue笔记[五]-路由
Vue笔记[六]-Vue3
Vite、Pinia、Router
QX-AI
GPT-4
QX-AI初始化中...
暂无预设简介,请点击下方生成AI简介按钮。
介绍自己
生成预设简介
推荐相关文章
生成AI简介

Vue简介

Vue 是一套用于构建用户界面渐进式 JavaScript 框架

构建用户界面: 在合适的时刻将数据应用到合适的位置
渐进式: Vue可以自底向上逐层的应用,即需要什么用什么,可以只用一个vue核心库,也可以装一堆插件库

Vue的特点:

  1. 组件化模式,提高代码复用率,让代码更好维护
  2. 声明式编码,无需直接操作DOM,提高开发效率
  3. 响应性,Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM
  4. 使用虚拟DOM和优秀的Diff算法,尽量复用DOM节点

Vue2API

体验Vue的方便:

命令式编码
1
2
3
4
5
let html = '';
data.forEach(item => {
html += `<li>${item.id}-${item.name}</li>`
})
document.getElementById('list').innerHTML = html;
声明式
1
2
3
4
5
<ul id="list">
<li v-for="item in data">
{{item.id}} - {{item.name}}
</li>
</ul>

下面的笔记是从Vue2开始的,直到Vue3

初次使用与理解

使用cdn或本地文件引入

1
2
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
<script src="/js/vue.js"></script>

引入后,全局就多了一个名为 Vue构造函数,它接收一个配置对象参数

初次使用步骤:

  1. 创建vue实例
  2. 挂载到容器
  3. 插值语法应用数据

思想:将数据交给Vue实例动态地去使用,而不去直接操作DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 容器 -->
<div id="root">
<!-- vue的插值语法 -->
<h1>hello {{msg}}</h1>
</div>

<script>
// 创建vue实例
new Vue({
el: '#root', // element元素,挂载到哪个容器
// 数据对象,只能给该容器使用
data: {
msg: 'chuckle'
}
});

// 效果
// hello chuckle
</script>

插值语法 {{ }} 中写的是js表达式(表达式即能生成一个值的代码),访问的数据以data为基准,当data中的数据发生改变,vue会自动重新解析模板更新数据

容器中的代码仍然符合html规范,vue会解析容器中的特殊语法(vue模板),然后应用数据和进行操作

注意:容器与实例是一一对应的,且不能嵌套容器,但一个容器和实例可以拆分成许多组件

模板语法

Vue有两种模板语法:

  1. 插值语法 {{ }}
  2. 指令语法 v-

插值语法用于标签体内容
指令语法用于解析标签(标签属性、标签体内容、绑定事件),数据都以data为基准

{{ }} 能看见(写) Vue 实例和原型上所有的属性

data数据对象中的键值对,最终都会通过数据代理作为 Vue 实例的属性

数据绑定

单向数据绑定 v-bind,只能由data去影响页面,可以简写为冒号
双向数据绑定 v-model,data和页面中数据变化都会互相影响

注意: v-model只能用于表单标签value属性上,v-model:value= 可以简写为 v-model=

v-bind 案例:数据绑定标签属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="root">
<!-- vue的插值语法 -->
<h1>hello {{msg}}</h1>
<!-- v-bind简写为冒号 -->
<a :href="url">前往首页</a>
</div>
<script>
// 创建vue实例
new Vue({
el: ".root",
data: {
msg: "chuckle",
url: "https://www.qcqx.cn/"
},
});
</script>

v-model案例:输入框内容变化会影响 data.msg 的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="root">
单向数据绑定<input type="text" :value="msg">
<br />
双向数据绑定<input type="text" v-model="msg">
</div>
<script>
// 创建vue实例
new Vue({
el: ".root",
data: {
msg: "chuckle"
},
});
</script>

挂载和数据写法

挂载容器有两种写法:配置对象中的 el 属性和实例对象的 $mount() 方法

tip: Vue实例和原型上以 $ 开头的属性和方法都是提供给程序员使用的

第一种
1
2
3
new Vue({
el: ".root"
});
第二种
1
2
const vm = new Vue({ });
vm.$mount('.root');

data数据也有两种写法:对象式函数式
函数式返回一个数据对象,组件中需用函数式,函数式中的this是Vue实例对象。由Vue所管理的函数都不要写成箭头函数

对象式
1
2
3
4
5
new Vue({
data: {
msg: "chuckle"
}
});
函数式
1
2
3
4
5
6
new Vue({
// data: function(),在对象属性值中写函数,一般删掉冒号和function
data(){
return{ msg: "chuckle" }
}
});

MVVM模型

Vue参考了MVVM模型

  1. M:模型(Model) - data 中的数据
  2. V:视图(View) - 模板
  3. VM:视图模型(ViewModel) - Vue 实例对象

Model 通过 VM 向 View 绑定数据,View 通过 VM 监听 Model 的数据变化

前端的框架大多都遵循这种模型,将数据放到指定位置,写好指定的模板代码,如何将数据和模板关联起来是框架的事,需要学习框架的语法

数据代理

数据代理:通过一个对象代理对另一个对象中属性的操作(读/写)

Vue2中使用 Object.defineProperty 实现数据代理,Vue3使用 Object.proxy() 以后再说

该方法可以向一个对象添加新属性或修改现有属性,且默认不可枚举、修改、删除

1
2
3
4
5
6
7
8
const obj = { name: 'chuckle' }
Object.defineProperty(obj, 'age', {
value: 19,
enumerable: true, // 使新添加的属性可被枚举,默认false
writable: true, // 控制属性是否能被修改,默认false
configurable: true // 控制属性是否能被删除,默认false
})

当 num 发生修改,obj.age 获取的值也会发生改变,当修改 obj.age ,num也会被修改,实现了两个对象间数据的双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
const obj = { name: 'chuckle' }
let num = 19;
Object.defineProperty(obj, 'age', {
// 当读取obj的age属性时,getter就会被调用,返回age的值
get() {
return num;
}
// 当修改obj的age属性时,setter就会被调用
set(value) {
num = value;
}
})

Vue中的数据代理:

配置对象 data 中的键值对都会通过数据代理给 vm 管理,当读取或修改属性时触发对应的 getter 和 setter

原来的data对象通过数据劫持变为 vm 上的 _data 属性,不直接赋值是为了实现响应式

触发 setter 后,_data里对应的属性也会发生改变,Vue监听_data有发生改变,让模板(页面)也发生改变

事件处理

v-on事件监听

使用 v-on 指令进行事件监听,可简写为@,@<事件名>="<回调函数名>",也可以写一些简单的语句

事件触发后的回调函数写在 methods 属性中,不做数据代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div class="root">
<button v-on:click="fun">{{msg}}</button>
<!-- v-on:可以简写为@ -->
<button @click="fun">{{msg}}</button>
</div>
<script>
new Vue({
el: ".root",
data: {
msg: "点击",
},
methods: {
fun() {
alert("chuckle");
},
},
});
</script>

v-on 可以传入参数,默认传入 event,后续传入参数会覆盖 event,但可以传入 $event 来获取事件对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="root">
<button @click="fun('你好', $event)">{{msg}}</button>
</div>
<script>
new Vue({
el: ".root",
data: {
msg: "点击",
},
methods: {
fun(str, e) {
alert(`${str}chuckle`);
console.log(e);
},
},
});
</script>

事件修饰符

v-on 有事件修饰符。使用: @<事件名>.<修饰符>
修饰符可以连着写多个 @<事件名>.<修饰符>.<修饰符>,分先后执行顺序

作用:为了让方法中数据逻辑更加的纯粹,而不用去处理 DOM 的逻辑细节

使用 prevent 修饰符代替 e.preventDefault() 阻止默认事件:

e.preventDefault()和prevent修饰符阻止默认事件
1
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
<!-- prevent修饰符 -->
<div class="root">
<a href="/" @click.prevent="fun">{{msg}}</a>
</div>
<script>
new Vue({
el: ".root",
data: {
msg: "点击",
},
methods: {
fun() {
console.log('点击');
},
},
});
</script>
<!-- e.preventDefault() -->
<div class="root">
<a href="/" @click="fun">{{msg}}</a>
</div>
<script>
new Vue({
el: ".root",
data: {
msg: "点击",
},
methods: {
fun(e) {
console.log('点击');
e.preventDefault();
},
},
});
</script>

常用的点击事件修饰符:

  1. prevent 阻止默认事件
  2. stop 阻止事件冒泡
  3. once 事件只触发一次
  4. capture 使用事件的捕获模式
  5. self 只有e.target是当前操作的元素才触发事件
  6. passive 事件的默认行为立即执行,无需等待回调函数执行完毕

鼠标修饰符:

  1. left 左键
  2. right 右键
  3. middle 中建

键盘事件

Vue中可以使用按键别名(修饰符)让特定按键去触发键盘事件,而无需使用e.key去判断

常用按键别名:

  1. 回车 enter
  2. 删除、退格 delete
  3. 退出 esc
  4. 空格 space
  5. 换行 tab(不适合keyup 适合keydown)
  6. 上 up 下 down 左 left 右 right
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="root">
<input type="text" @keyup.enter="fun" placeholder="回车输入"></input>
</div>
<script>
new Vue({
el: ".root",
methods: {
fun(e) {
console.log(e.target.value);
},
},
});
</script>

也可以通过 .键名 来绑定其它按键

双驼峰的键名需要写成小写再用横杆连接,如 CapsLock 要写成 caps-lock

1
2
3
@keyup.a="fun" // 绑定字母A键
@keyup.caps-lock="fun" // 绑定切换大小写键
@keyup.Control="fun" // 绑定ctrl键

系统修饰键:ctrl、alt、shift、meta
(1) 配合kyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发
(2) 配合kydown使用:正常触发

自定义键盘别名(不推荐使用):

1
Vue.config.keyCodes.自定义键名=键码

技巧:利用keyup系统修饰键特性绑定组合键

1
2
@keyup.ctrl.a="fun" // 绑定ctrl+a,先按下ctrl再按下a,然后释放a才触发
@keyup.alt.q="fun" // 绑定alt+q

计算属性computed

计算属性:通过已有的数据(属性)加工出一个新的属性

computed 配置项中定义计算属性,计算属性要写成对象形式,并添加getter和setter(非必须)
使用this访问vm上的属性

计算属性会在第一次被调用后被缓存,即getter只触发一次,后续使用该属性会从缓存中拿
当计算属性所依赖数据(在vm上的)发生变化时,会再次调用getter更新缓存

案例:通过姓和名计算出全名

1
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
<div class="root">
姓:<input type="text" v-model="firstName"><br />
名:<input type="text" v-model="lastName"><br />
全名:<input type="text" v-model="fullName"><br />
</div>
<script>
new Vue({
el: ".root",
data: {
firstName: "张",
lastName: "三"
},
// 计算属性配置项
computed:{
// 计算属性
fullName:{
get(){
return `${this.firstName}-${this.lastName}`
},
set(name){
let arr = name.split("-");
this.firstName = arr[0];
this.lastName = arr[1];
}
}
}
});
</script>

当计算属性只读不写时,可以简写,直接将计算属性值写成一个函数,作为getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Vue({
el: ".root",
data: {
firstName: "张",
lastName: "三"
},
// 计算属性配置项
computed:{
// 计算属性
fullName(){
return `${this.firstName}-${this.lastName}`
}
}
}
});

当计算量非常大的时候,而且访问量次数非常多,改变的时机却很小,那就需要用到computed;缓存会减少很多计算量

computedmethods 区别:

  1. computed是属性访问,而methods是函数调用。computed带有缓存功能,而methods没有
  2. computed是响应式的,methods并非响应式
  3. computed是以对象的属性方式存在的,在视图层直接调用就可以得到值,如:{{msg}} ,而methods必须以函数形式调用,例如:{{msg()}} ,computed直接以对象属性方式调用,而methods必须要函数执行才可以得到结果
  4. computed带缓存,只有依赖数据发生改变,才会重新进行计算,而methods里的函数在每次调用时都要执行
  5. computed不支持异步,当computed内有异步操作时无效,无法监听数据的变化

监视属性watch

watch 配置项中定义监视属性,监视某一属性的变化,监视属性本身也是一个配置项

可以监视data中的属性,也可以监视计算属性,若属性不存在也不会报错

作用:当属性值发生变化后进行某些操作

1
2
3
4
5
watch: {
<属性名>: {
<监视配置项>
}
}
1
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
<div class="root">
<h2>今天天气: {{weather}}</h2>
<button @click="changeWeather">切换天气</button>
</div>
<script>
const vm = new Vue({
el: ".root",
data: {
isHot: true,
},
computed: {
weather(){
return this.isHot ? "炎热" : "凉爽"
}
},
methods: {
changeWeather(){
this.isHot = !this.isHot;
}
},
watch: {
weather: {
// 初始化时调用一次handler
immediate: true,
// 当监视属性发生变化时触发handler
handler(newValue, oldValue){
// 传入属性新老值两个参数
console.log(newValue, oldValue);
}
}
}
});
</script>

也可以通过vm的 $watch() 方法来监视某个属性

1
2
3
4
5
6
7
8
9
10
vm.$watch("weather", {
// 初始化时调用一次handler
immediate: true,
// 当监视属性发生变化时触发handler
handler(newValue, oldValue) {
// 传入属性新老值两个参数
console.log(newValue, oldValue);
},
});

深度监视:

当监视多级结构中的某个属性时,属性名要写成字符串,或者 num[a]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const vm = new Vue({
el: ".root",
data: {
num: {
a: 1,
b: 2
}
},
watch: {
// 深度监视写成字符串
'num.a': {
// 初始化时调用一次handler
immediate: true,
// 当监视属性发生变化时触发handler
handler(newValue, oldValue){
// 传入属性新老值两个参数
console.log(newValue, oldValue);
}
}
}
});

监视整个多级结构所有属性时,要添加配置项 deep: true,handler参数是该num对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const vm = new Vue({
el: ".root",
data: {
num: {
a: 1,
b: 2
}
},
watch: {
// 深度监视写成字符串
num: {
// 监视整个num和num里的属性
deep: true,
// 初始化时调用一次handler
immediate: true,
// 当监视属性发生变化时触发handler
handler(newValue, oldValue){
// 传入属性新老值两个参数
console.log(newValue, oldValue);
}
}
}
});

当监视属性配置中只有 handler 方法时,可以简写

1
2
3
4
5
6
7
8
9
10
watch: {
weather(newValue, oldValue){
console.log(newValue, oldValue);
}
}
// 不再传一个配置对象,直接传一个函数作为handler
vm.$watch("weather", function(newValue, oldValue){
console.log(newValue, oldValue);
});

注意:计算属性多个影响一个的时候用,监听属性一个影响多个或有复杂(异步)业务的时候用

绑定class样式

通过 v-bind 动态得将样式class应用到盒子上

每个标签只能有一个 class 属性,但vue会自动将绑定的 class 合并到现有的 class 属性上

有三种绑定形式:

  1. 字符串写法,适用于样式的类名不确定,需要动态指定
  2. 数组写法,适用于要绑定的样式个数不确定,名字也不确定
  3. 对象写法,适用于要绑定的样式个数、名字确定,但需要动态决定用不用
1
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
.basic {
border: 1px solid #222;
margin: 20px;
}
/* 控制盒子大小,互斥 */
.big {
width: 300px;
height: 300px;
}
.small {
width: 100px;
height: 100px;
}
.normal {
width: 200px;
height: 200px;
}
/* 控制其它样式,可同时使用 */
.fcolor {
color: rgb(227, 57, 57);
}
.bg {
background: #ccc;
}
.bdcolor {
border-color: rgb(20, 153, 149);
}

1
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
<div id="root">
<!-- 字符串写法,适用于样式的类名不确定,需要动态指定 -->
<div class="basic" :class="size" @click="changeSize">{{msg}}</div>
<!-- 数组写法,适用于要绑定的样式个数不确定,名字也不确定 -->
<div class="basic normal" :class="classArr">{{msg}}</div>
<!-- 对象写法,适用于要绑定的样式个数确定,名字也确定,但需要动态决定用不用 -->
<div class="basic normal" :class="classObj">{{msg}}</div>
</div>
<script>
let sizeArr = ["small", "normal", "big"];
new Vue({
el: "#root",
data: {
msg: "chuckle",
size: "normal",
classArr: ["fcolor", "bg", "bdcolor"],
classObj: {
// true则应用样式,false不应用
fcolor: false,
bg: true,
bdcolor: true,
},
},
methods: {
//点击随机切换大小
changeSize() {
this.size = sizeArr[Math.floor(Math.random() * sizeArr.length)];
},
},
});
</script>

绑定style行内样式

两种写法:对象写法、数组写法。对象中属性名单驼峰

1
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
<div id="root">
<!-- 对象写法 -->
<div class="basic" :style="styleObj">{{msg}}</div>
<!-- 数组写法 -->
<div class="basic" :style="styleArr">{{msg}}</div>
</div>
<script>
new Vue({
el: "#root",
data: {
msg: "chuckle",
styleObj: {
fontSize: '40px',
color: '#363636',
background: '#ccc',
},
// 数组内写样式对象,一般不用
styleArr: [
{
fontSize: '40px',
},
{
background: '#ccc',
}
]
}
});
</script>

条件渲染v-if

条件渲染,符合某些条件才渲染元素,即通过指令去控制元素的显隐,不同指令实现原理不同

v-show="<表达式>" 表达式结果需能表示为布尔值

原理:使用 display:'none' 隐藏元素,节点仍在

v-if="<表达式>" 表达式结果需能表示为布尔值

原理:在模板解析时不渲染该元素,节点不存在,会用一个空注释占位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="root">
<!-- v-show -->
<div v-show="isShow">{{msg}}</div>
<!-- v-if -->
<div v-if="isShow">{{msg}}</div>
</div>
<script>
new Vue({
el: "#root",
data: {
msg: "chuckle",
isShow: false,
},
});
</script>

当显隐切换频率高使用 v-show 性能更好

v-else-ifv-else 需配合 v-if 使用,效果和js中 if-else 一样

使用这两个指令需要div结构相邻

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="root">
<!-- v-else-if -->
<div v-if="n === 1">{{n}}</div>
<!-- 当if匹配到一个就不会再往后判断 -->
<div v-else-if="n === 1">重复{{n}}</div>
<div v-else-if="n === 2">{{n}}</div>
<div v-else-if="n === 3">{{n}}</div>
<!-- v-else不用写表达式 -->
<div v-else>{{n}}</div>
</div>
<script>
new Vue({
el: "#root",
data: {
msg: "chuckle",
n: 1
},
});
</script>

当多个标签显隐条件一样时,可以使用 <template> 标签包裹,在该标签中写 v-if ,实际html结构中不会存在该标签

1
2
3
4
5
<template v-if="n===1">
<h1>{{msg}}</h1>
<h2>{{msg}}</h2>
<h3>{{msg}}</h3>
</template>

列表渲染v-for

v-for 将数组或对象中一系列重复、相似的数据渲染到页面中,这些数据应该要有唯一标识(通常是id)

使用方法类似js中的 for-in

使用 v-for 遍历渲染需要在标签内绑定上 key 属性作为唯一标识

用法
1
2
3
4
5
<ul>
<!-- 参数1 p是persons中的元素,参数2 index/key是索引,遍历数组时是从0开始的数组下标,遍历对象时是每个属性名-->
<!-- key标签属性是唯一标识 -->
<li v-for="(p,index) in persons" :key="p.id">{{index}} - {{p.name}} - {{p.age}}</li>
</ul>

遍历数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="root">
<ul>
<li v-for="(p,index) in persons" :key="p.id">{{index}} - {{p.name}} - {{p.age}}</li>
</ul>
</div>
<script>
new Vue({
el: "#root",
data: {
msg: "chuckle",
// 需渲染的数据
persons: [
{ id: '001', name: 'chuckle', age:'19'},
{ id: '002', name: 'qx', age:'18'},
{ id: '003', name: 'giggles', age:'20'},
],
},
});
</script>

遍历对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="root">
<ul>
<li v-for="(p,key) in commodity" :key="key">{{key}} - {{p}}</li>
</ul>
</div>
<script>
new Vue({
el: "#root",
data: {
msg: "chuckle",
// 需渲染的数据
commodity: {
name: '商品1',
price: 20,
place: '中国'
}
},
});
</script>

key

v-for 遍历渲染时需要给每个标签绑定 key 属性,作为唯一标识

key 在 vue 内部虚拟DOM上使用,解析模板时不会渲染到页面上

作用:vue在解析模板到真实DOM上时,会先将数据转为虚拟DOM,当数据发生变化,又会再生成一次虚拟DOM,然后会用diff算法对比新旧两份虚拟DOM,对比过程依赖 key,将key相同的标签内所有属性和节点单独进行对比,将相同的节点进行dom复用,不同的则新覆盖旧,新虚拟DOM中没有的key则删除对应标签

diff算法: 对比两个新旧虚拟dom(两棵树)之间的差异的一种算法

选择key:
使用后端维护的唯一标识,id、手机号、学号等

若不手动绑定key,vue会自动将index作为key
一般不使用 index 作为key,因为index不与数据一一对应,会导致diff算法对比时发生问题(逆序操作时无法正常复用dom、虚拟dom中不存在的用户输入[input]会发生错位)

如果不存在对数据的逆序添加、逆序明除等破坏顺序操作,仅用于渲染列表用于展示,可以使用index作为key

列表过滤

业务中经常需要对原数据进行搜索过滤后再展示,可以使用计算属性实现

若业务较复杂也可使用监视属性实现,但应优先考虑计算属性

1
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
<div id="root">
<!-- 列表过滤 -->
<input type="text" placeholder="模糊搜索姓名" v-model="keyWord">
<ul>
<li v-for="(p,index) in fillPersons" :key="p.id">
{{index+1}} - {{p.name}} - {{p.age}} - {{p.sex}}
</li>
</ul>
</div>
<script>
new Vue({
el: "#root",
data: {
msg: "chuckle",
// 搜索的关键字
keyWord: "",
// 需渲染的数据
persons: [
{ id: "001", name: "张三", age: "19", sex: "男" },
{ id: "002", name: "李三", age: "18", sex: "女" },
{ id: "003", name: "李四", age: "20", sex: "女" },
{ id: "004", name: "刘四", age: "18", sex: "男" },
]
},
computed: {
// 通过搜索关键字和原数据计算出的数据
fillPersons: {
get(){
return this.persons.filter((item)=>{
return item.name.indexOf(this.keyWord) !== -1;
})
}
}
}
});
</script>

列表排序

实际业务中多是后端排好序,但前端排序有时也会用到

列表排序和列表过滤通常一起用

1
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
<div id="root">
<!-- 列表过滤和排序 -->
<input type="text" placeholder="模糊搜索姓名" v-model="keyWord" />
<br />
<button @click="sortType=1">年龄升序</button>
<button @click="sortType=-1">年龄降序</button>
<button @click="sortType=0">原序</button>
<ul>
<li v-for="(p,index) in fillPersons" :key="p.id">
{{index+1}} - {{p.name}} - {{p.age}} - {{p.sex}}
</li>
</ul>
</div>
<script>
new Vue({
el: "#root",
data: {
msg: "chuckle",
// 控制排序类型,0原序1升-1降
sortType: 0,
// 搜索关键字
keyWord: "",
// 需渲染的数据
persons: [
{ id: "001", name: "张三", age: 19, sex: "男" },
{ id: "002", name: "李三", age: 18, sex: "女" },
{ id: "003", name: "李四", age: 21, sex: "女" },
{ id: "004", name: "刘四", age: 20, sex: "男" },
],
},
computed: {
// 通过搜索和排序类型计算出的数据
fillPersons: {
get() {
// 根据关键字筛选
let arr = this.persons.filter((item) => {
return item.name.indexOf(this.keyWord) !== -1;
});
// 进行排序
if (this.sortType) arr.sort((a, b) => this.sortType * (a.age - b.age));
return arr;
},
},
},
});
</script>

监视数据(响应式)原理

Vue 会监视 data 中所有层次的数据。当数据发生改变,会重新解析模板,也就是数据的响应式

这种监视是通过每个属性对应的 setter 实现的,在 setter 中对数据进行实际的修改、重新解析模板等操作

数据劫持

对 data 中的数据进行加工,添加 getter、setter 以实现某些交互功能(响应式) 就是数据劫持

数据代理的区别:数据代理是通过一个对象代理对另一个对象中属性的操作(读/写),而数据劫持是对一个对象中原有的属性进行加工

利用数据劫持简单复刻一下 Vue 的响应式:

1
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
// data数据
let data = {
name: 'chuckle',
age: 19
}
// 创建监视实例对象,监视data中数据变化
const obs = new Observer(data)

// vue实例对象
let vm = {}
// 将obs引用地址赋给vm._data和data
// 此刻就完成了数据劫持,原来的data的地址只有当时传入Observer的参数obj才知道
// data只是地址发生了改变,但原来的数据仍然在一个地址中,被obs所管理
vm._data = data = obs

// 监视构造函数,能创建监视实例对象
// 传入要监视的对象
function Observer(obj) {
// 拿到对象后提取所有属性名到一个数组中
const keys = Object.keys(obj)
// 遍历keys,加工属性
keys.forEach((k)=>{
// 和数据代理一样,用到defineProperty
// 构造函数中的this是其实例对象
// 这一步相当于将data上的数据代理到实例对象上
Object.defineProperty(this,k,{
// 添加getter、setter。实现属性的读写
get(){
return obj[k]
},
set(val){
obj[k] = val
// 修改完值后,就可以去重新解析模板
parsingTemplates()
}
})
})
}
// vue重新解析模板的函数实现,怎么实现的暂时不研究
function parsingTemplates(){
console.log('重新解析模板');
}

输出 vm._data 对象:

1
2
3
4
5
6
7
8
9
Observer {}
age: (…) // 19
name: (…) // 'qx'
get age: ƒ get()
set age: ƒ set(val)
get name: ƒ get()
set name: ƒ set(val)
[[Prototype]]: Object

实现效果:当修改 vm._data 中的数据,控制台会输出【重新解析模板】

发生了什么:通过数据代理将 data 中数据的读和写操作代理到了监视实例对象 obs 身上,并且在 setter 中调用解析模板的函数,这是实现响应式的关键,然后将 obs 引用地址赋值给 vm._data,上面这些步骤就实现了数据劫持,即 Vue 中并不是直接将 data 赋值给 vm._data,而是先劫持 data,对 data 进行加工后,将加工好的 obs 赋给 vm._data

后面就是正常的数据代理操作,将 vm._data 通过数据代理到 vm 身上,方便操作

注意:Vue中的响应式实现要比这完善得多

  1. 没有考虑data数据是多层次情况,Vue会找到data中所有层次的对象的属性进行加工
  2. 数组中元素的响应式

$set()追加属性

通过之前的操作 Vue 可以响应式地监视 data 数据

但如果不是编写代码时就写在 data 中的数据,而是之后通过 . 动态添加的属性,Vue则无法监视,修改这些属性值,模板不会重新解析,页面不会变化

可以通过 Vue.set()vm.$set() 动态添加可被 Vue 监视的属性
传入三个参数 (<目标对象>, <属性名>, <属性值>)

作用:向响应式对象中添加一个 property(属性),并让这个新 property 同样是响应式的,且触发视图更新。

注意:目标对象不能是data或vm实例,只能是其中的某个实际存在的对象,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const vm = new Vue({
el: '#root',
data: { student: {} }
})
// 追加属性
Vue.set(vm.student, 'name', 'chuckle')
// $set()还能用于修改属性值
// 一般是在修改数组元素后为了能被Vue监视到这次修改而使用
vm.$set(vm.student, 'name', 'qx')

// 不能直接在data或vm实例上添加属性
vm.$set(vm, 'name', 'qx')
vm.$set(vm._data, 'name', 'qx')

// 直接添加不会被Vue监视,无法响应式
vm._data.student.name = 'chuckle'
vm.student.name = 'chuckle'

对数组的监视

JS中数组也是一个对象,但它不能添加getter和setter,也就是不能通过数据劫持来实现数组中元素的响应式,无法触发视图更新

Vue 无法在元素被通过索引值修改后,响应式地重新解析模板,触发视图更新

1
2
3
4
5
6
const vm = new Vue({
el: '#root',
data: { student: ['001', 'chuckle', '19'] }
})
// 修改student数组中第一个元素,不会触发视图更新
vm.student[0] = '002';

Vue对数组元素的监视:将被监视的数组的变更方法进行了包装,调用这些变更方法就会触发视图更新

变更方法:push()、pop()、shift()、unshift()、splice()、sort()、reverse()

包装:在Vue中的数组上重写同名的变更方法,在重写的方法中也会调用原来 Array 上的变更方法,但还加入了视图更新等操作

也可以通过 $set() 在变更数组时也触发视图更新:

1
2
// 修改第二个元素
vm.$set(vm.student, 1, 'qx')

收集表单数据

v-model 对不同表单元素有特殊的处理

  1. < input type=’text’/> 文本输入框 v-model 收集 value 值,即收集用户在输入框的输入
  2. < input type=’radio’/> 单选框 v-model 收集 value 值,但要给标签配置 value 值
  3. < input type=’checkbox’/> 多选框
    1. 没有配置value值,则默认收集checked,一个被勾选会导致全部被勾选
    2. 配置value值后,v-model初始值为数组,收集的是value值作为元素到数组中,初始值为空字符串,则收集checked

v-model 也有修饰符,对收集的数据进行简单处理

  1. lazy 失去焦点再收集数据
  2. number 输入字符串转为有效数字,通常和 < input type=’number’/> 一起使用
  3. trim 去除字符串首尾空格
案例:用户信息收集
1
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
<div id="root">
<!-- 绑定提交事件,后续发送ajax请求 -->
<form @submit.prevent="submit">
用户名: <input type="text" v-model.trim="formInfo.username" /><br />
密码: <input type="password" v-model="formInfo.password" /><br />
<!-- 数字输入 -->
年龄: <input type="number" v-model.number="formInfo.age" /><br />
<!-- 单选框 -->
性别:
<label for="man"></label>
<input type="radio" v-model="formInfo.sex" name="sex" id="man" value="男"/>
<label for="woman"></label>
<input type="radio" v-model="formInfo.sex" name="sex" id="woman" value="女"/><br />
<!-- 多选框 -->
爱好:
<label for="hobbyOne">吃饭</label>
<input type="checkbox" v-model="formInfo.hobby" name="hobby" id="hobbyOne" value="吃饭"/>
<label for="hobbyTwo">睡觉</label>
<input type="checkbox" v-model="formInfo.hobby" name="hobby" id="hobbyTwo" value="睡觉"/>
<label for="hobbyThree">打豆豆</label>
<input type="checkbox" v-model="formInfo.hobby" name="hobby" id="hobbyThree" value="打豆豆"/><br />
<!-- 下拉框 -->
城市:
<select v-model="formInfo.city">
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="广州">广州</option>
</select><br />
其他信息:
<textarea v-model.lazy="formInfo.other"></textarea><br />
<input type="checkbox" v-model="formInfo.agree" name="agree"/>接受用户协议<br />
<button>提交</button>
</form>
</div>

<script>
new Vue({
el: "#root",
data: {
// 表单信息对象
formInfo: {
username: "",
password: "",
age: "",
sex: "",
hobby: [],
city: "",
other: "",
agree: false,
},
},
methods: {
submit() {
// 输出收集的表单数据并转为json格式
console.log(JSON.stringify(this.formInfo));
},
},
});
</script>

过滤器

Vue3中已废弃过滤器

filters 中配置局部过滤器,Vue.filter() 中配置全局过滤器

和计算属性一样,过滤器也是对现有数据进行加工,本质上是函数

使用: {{ 数据 | 过滤器1 | 过滤器2 }} 管道符分隔,会依次传参调用,也可以在指令语法中使用,前一个的结果作为后面过滤器的第一个参数,返回这作为结果,还可以再传额外的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="root">
<h3>时间:{{ time | fmtTime | interceptStr }}</h3>
</div>
<script>
// 全局过滤器
Vue.filter('interceptStr',(value, num=4)=>{
return value.slice(0,num)
})
new Vue({
el: '#root',
data: {
time: 1683449879865,
},
// 局部过滤器
filters: {
fmtTime(value, str='YYYY-MM-DD HH:mm:ss'){
return dayjs(value).format(str);
}
}
})
</script>

常见内置指令

前面已经学习了不少内置指令,v-on、v-bind、v-model、v-show、v-if、v-for等

Vue中还有一些常见的内置指令:

  1. v-text innerText同款效果
  2. v-html innerHtml同款效果,需注意安全性问题xss攻击
  3. v-clock 当Vue接管容器时,删除所有v-clock属性,配合css属性选择器实现隐藏未编译的 Mustache 标签直到实例准备完毕,或加载动画
    1
    [v-cloak] { display: none; }
  4. v-once 只渲染元素和组件一次,后续重新解析模板渲染页面会直接复用带有v-once的标签,视为静态内容,这可以用于优化更新性能
  5. v-pre 跳过这个元素和它的子元素的编译过程,用于跳过没有指令和插值语法的节点,能加快编译。

自定义指令

指令通过操控dom来控制交互或样式,如 v-show 通过操控css display 属性,来控制元素显隐

Vue的指令就是将原生操控dom的JS代码进行了封装

自定义指令需要亲自去写操控 dom 的JS代码

注册指令:
自定义指令函数式传入两个参数

  1. el 指令所在的标签dom元素
  2. binding 一个对象,包含绑定相关的信息,其中 value 属性值是表达式的值

分为全局指令局部指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 注册全局指令
Vue.directive('<指令名>', function(el, binding){
el.innerHTML = binding.value
})
// 注册局部指令
new Vue({
el: '#root',
data: { },
directives: {
'<指令名>'(el, binding){
el.innerHTML = binding.value
}
}
})

指令名的问题:
指令名不应该写成驼峰形式,要用横杠形式。若指令名为单驼峰形式,使用指令时会找不到指令

1
2
3
4
5
directives: {
'num-tenfold'(el, binding){ }
}
// 使用指令:
// <span v-num-tenfold="n"></span>

注意:指令中的this指向window

指令内容写法

自定义指令的内容有两种写法:对象式、函数式

1、对象式:

1
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
<div id="root">
<h3>n值:{{n}}</h3>
<h3>
放大十倍n值:
<!-- v-tenfold让n值放大10倍再应用到标签内容上 -->
<span v-tenfold="n"></span>
</h3>
<button @click="n++">让n+1</button>
</div>
<script>
new Vue({
el: '#root',
data: {
n: 0
},
// 注册局部指令
directives: {
// 指令名不用带v-
tenfold: {
// 指令与元素成功绑定(还未放到页面上)时调用
bind(el, binding){
el.innerHTML = binding.value * 10;
},
// 元素应用到页面后,进行dom相关操作
inserted(el, binding){
// 让其父元素背景变灰色
el.parentNode.style.background = "#ccc";
},
// 指令所在的模板被重新解析时调用
update(el, binding) {
el.innerHTML = binding.value * 10;
},
}
}
})
</script>

对象式指令被调用:

  1. 指令与元素成功绑定(还未放到页面上)时,调用对象中 bind() 函数
  2. 指令所在元素被插入页面上时,调用对象中 inserted() 函数
  3. 指令所在的模板被重新解析时,调用对象中 update() 函数

Vue在不同时刻会调用指令对象中不同的函数

2、函数式:

1
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
<div id="root">
<h3>n值:{{n}}</h3>
<h3>
放大十倍n值:
<!-- v-tenfold让n值放大10倍再应用到标签内容上 -->
<span v-tenfold="n"></span>
</h3>
<button @click="n++">让n+1</button>
</div>
<script>
new Vue({
el: '#root',
data: {
n: 0
},
// 注册局部指令
directives: {
// 指令名不用带v-
tenfold(el, binding){
el.innerHTML = binding.value * 10;
// 因为没有inserted状态,且在bind状态时获取不到dom元素
// 父元素一开始并不会应用此灰色背景
// 后续重新解析模板时就有效果,因为元素已被应用可获取到父元素
el.parentNode.style.background = "#ccc";
}
}
})
</script>

函数式将对象式的 bind()update() 函数合二为一,Vue2中没有合并上 inserted() 函数的效果,即Vue2中函数式里一般不能进行dom相关操作

函数式指令被调用:

  1. 指令与元素成功绑定(还未放到页面上)时
  2. 指令所在的模板被重新解析时