三、Vue基础语法 之 模板语法的深入学习
Lyk 2022/8/22 Vue计算属性computed侦听器watch双向数据绑定v-model表单
# 1、复杂data的处理方式
- 我们知道,在模板中可以直接通过插值语法显示一些data中的数据。
- 但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示;
- 比如我们需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示;
- 在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算;
- 在模板中放入太多的逻辑会让模板过重和难以维护;
- 并且如果多个地方都使用到,那么会有大量重复的代码;
- 我们有没有什么方法可以将逻辑抽离出去呢?
- 可以,其中一种方式就是将逻辑抽取到一个method中,放到methods的options中;
- 但是,这种做法有一个直观的弊端,就是所有的data使用过程都会变成了一个方法的调用;
- 另外一种方式就是使用计算属性computed;
# 2、认识计算属性computed
- 什么是计算属性呢?
- 官方并没有给出直接的概念解释;
- 而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性;
- 计算属性将被混入到组件实例中
- 所有 getter 和 setter 的 this 上下文自动地绑定为组件实例;
- 计算属性的用法:
- 选项:computed
- 类型:{ [key: string]: Function | { get: Function, set: Function } }
- 那接下来我们通过案例来理解一下这个计算属性。
# 3、案例实现思路
# 3.1.案例实现思路
- 我们来看三个案例:
- 案例一:我们有两个变量:firstName和lastName,希望它们拼接之后在界面上显示;
- 案例二:我们有一个分数:score
- 当score大于60的时候,在界面上显示及格;
- 当score小于60的时候,在界面上显示不及格;
- 案例三:我们有一个变量message,记录一段文字:比如Hello World
- 某些情况下我们是直接显示这段文字;
- 某些情况下我们需要对这段文字进行反转;
- 我们可以有三种实现思路:
- 思路一:在模板语法中直接使用表达式;
- 思路二:使用method对逻辑进行抽取;
- 思路三:使用计算属性computed;
# 3.2.实现思路一:模板语法实现
- 思路一的实现:模板语法
- 缺点一:模板中存在大量的复杂逻辑,不便于维护(模板中表达式的初衷是用于简单的计算);
- 缺点二:当有多次一样的逻辑时,存在重复的代码;
- 缺点三:多次使用的时候,很多运算也需要多次执行,没有缓存;
<div id="app">
<h2>{{ firstName + ' ' + lastName }}</h2>
<h2>{{ score >= 60 ? '及格' : '不及格' }}</h2>
<h2>{{ message.split(" ").reverse().join(" ") }}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
firstName: 'Stephen',
lastName: 'curry',
score: 80,
message: "my name is kobe"
}
}
}).mount("#app")
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 3.3.实现思路二:method实现
- 思路二的实现:method实现
- 缺点一:我们事实上先显示的是一个结果,但是都变成了一种方法的调用;
- 缺点二:多次使用方法的时候,没有缓存,也需要多次计算;
<div id="app">
<h2>{{ fullName() }}</h2>
<h2>{{ result() }}</h2>
<h2>{{ reverseMessage() }}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
firstName: 'Stephen',
lastName: 'curry',
score: 80,
message: "my name is kobe"
}
},
methods: {
fullName() {
return this.firstName + this.lastName
},
result() {
return this.score >= 60 ? '及格' : '不及格'
},
reverseMessage() {
return this.message.split(" ").reverse().join(" ")
}
}
}).mount("#app")
</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
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
# 3.4.思路三的实现:computed实现
- 思路三的实现:computed实现
- 注意:计算属性看起来像是一个函数,但是我们在使用的时候不需要加(),这个后面讲setter和getter时会讲到;
- 我们会发现无论是直观上,还是效果上计算属性都是更好的选择;
- 并且计算属性是有缓存的;
<div id="app">
<h2>{{ fullName }}</h2>
<h2>{{ result }}</h2>
<h2>{{ reverseMessage }}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
firstName: 'Stephen',
lastName: 'curry',
score: 80,
message: "my name is kobe"
}
},
computed: {
fullName() {
return this.firstName + this.lastName
},
result() {
return this.score >= 60 ? '及格' : '不及格'
},
reverseMessage() {
return this.message.split(" ").reverse().join(" ")
}
}
}).mount("#app")
</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
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
# 4、计算属性 vs methods
- 在上面的实现思路中,我们会发现计算属性和methods的实现看起来是差别是不大的,而且我们多次提到计算属性有缓存的。
- 接下来我们来看一下同一个计算多次使用,计算属性和methods的差异:
- 你会发现,在依赖数据没有发生变化的情况下,计算属性computed使用了三次,但是只计算了一次;也就是只调用了一次getter方法获取返回结果【计算属性有缓存,第二次使用时,在依赖数据不发生变化的情况下,会直接根据缓存拿到结果】
- 而methods使用了三次,就调用了三次,
# 5、计算属性的缓存
- 这是什么原因呢?
- 这是因为计算属性会基于它们的依赖关系进行缓存;
- 在数据不发生变化时,计算属性是不需要重新计算的;
- 但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算;
<div id="app">
<h2>{{ result }}</h2>
<h2>{{ result }}</h2>
<h2>{{ result }}</h2>
<h2>{{ getResult() }}</h2>
<h2>{{ getResult() }}</h2>
<h2>{{ getResult() }}</h2>
<button @click="score = 52">点击此按钮让依赖的数据发生变化</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
score: 80
}
},
computed: {
result() {
console.log('依赖的数据score:',this.score)
console.log('调用了计算属性result的getter')
return this.score >= 60 ? '及格' : '不及格'
}
},
methods: {
getResult() {
console.log('调用了methods中的getResult方法')
return this.score >= 60 ? '及格' : '不及格'
}
}
}).mount("#app")
</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
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
# 6、计算属性的setter和getter
- 计算属性在大多数情况下,只需要一个getter方法即可,所以我们会将计算属性直接写成一个函数。
<div id="app">
<h2>促销价格:{{ priceComp1 }}</h2>
<h2>促销价格:{{ priceComp2 }}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
price: 33
}
},
computed: {
priceComp1: {
get() {
return '¥' + this.price.toFixed(2)
}
},
priceComp2() {
return '¥' + this.price.toFixed(2)
}
}
}).mount("#app")
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 但是,如果我们确实想设置计算属性的值呢?
- 这个时候我们也可以给计算属性设置一个setter的方法;
<div id="app">
<h2>{{ fullName }}</h2>
<button @click="fullName = 'lyk' ">按钮1</button>
<button @click="fullName = 'kobe bryant' ">按钮2</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
firstName: 'Stephen',
lastName: 'curry',
}
},
computed: {
fullName: {
get() {
return this.firstName + " " + this.lastName
},
set(value) {
const name = value.split(" ")
this.firstName = name[0]
this.lastName = name[1]
}
}
}
}).mount("#app")
</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
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
# 7、源码如何对setter和getter处理呢?(了解)
- 你可能觉得很奇怪,Vue内部是如何对我们传入的是一个getter,还是说是一个包含setter和getter的对象进行处理的呢?
- 事实上非常的简单,Vue源码内部只是做了一个逻辑判断而已
# 8、认识侦听器watch
- 什么是侦听器呢?
- 开发中我们在data返回的对象中定义了数据,这个数据通过插值语法等方式绑定到template中;
- 当数据变化时,template会自动进行更新来显示最新的数据;
- 但是在某些情况下,我们希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器watch来完成了;
- 侦听器的用法如下:
- 选项:watch
- 类型:{ [key: string]: string | Function | Object | Array}
# 9、侦听器案例
- 举个栗子(例子):
- 比如现在我们希望用户在input中输入一个问题;
- 每当用户输入了最新的内容,我们就获取到最新的内容,并且使用该问题去服务器查询答案;
- 那么,我们就需要实时的去获取最新的数据变化;
<div id="app">
<input type="text" v-model.lazy="question" placeholder="请输入问题">
<span>{{question}}</span>
<h2>答案:{{message}}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
question: '',
message: ''
}
},
watch: {
question(newValue, oldValue) {//写法一 : 方法写法
console.log(newValue)
this.getAnwser(newValue)
},
question: function (newValue, oldValue) {//写法一
console.log(newValue)
this.getAnwser(newValue)
},
question: {
handler(newValue,oldValue) {//写法二: 对象写法
console.log(newValue,oldValue)
this.getAnwser(newValue)
},
// immediate:true
},
question: "questionMethod",//写法三:字符串 结合 methods方法
question:['questionMethod','questionMethod1']// 写法四:数组 结合methods 写法;如果侦听的数据发生改变,那么数组中对应methods中的方法都将被逐一调用
},
methods: {
getAnwser(question) {
this.message = `${question}的问题答案是:哈哈哈`
},
questionMethod(newValue, oldValue) {
console.log(newValue, oldValue)
this.getAnwser(newValue)
},
questionMethod1(newValue, oldValue) {
console.log('侦听到了question数据发生变化了')
}
}
}).mount("#app")
</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
48
49
50
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
# 10、侦听器watch的配置选项
- 我们先来看一个例子:
- 当我们点击按钮的时候会修改info.name的值;
- 这个时候我们使用watch来侦听info,可以侦听到吗?答案是不可以。
- 这是因为默认情况下,watch只是在侦听info的引用变化,对于内部属性的变化是不会做出响应的:
- 这个时候我们可以使用一个选项deep进行更深层的侦听;
- 注意前面我们说过watch里面侦听的属性对应的也可以是一个Object;
- 还有另外一个属性,是希望一开始的就会立即执行一次(即立即侦听一次):
- 这个时候我们使用immediate选项;
- 这个时候无论后面数据是否有变化,侦听的函数都会先执行一次;
- 补充:在watch中侦听对应的数据变化;我们用对象写法时,需要在对象中定义一个
handler方法
该方法名字是固定的(vue内部实现的);在侦听的数据发生改变时,会自动回调对象中的handler方法;
<div id="app">
<h2>{{info}}</h2>
<button @click="info.name = 'james'">按钮</button>
<button @click="info = {name:'curry'}">按钮</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
info: {
name:'kobe',
age:44
}
}
},
watch: {
info: {//对象写法
handler(newValue,oldValue) {
console.log('侦听info:',newValue,oldValue)
},
deep:true,
immediate:true
}
}
}).mount("#app")
</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
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
# 11、侦听器watch的其他方式(一)
<div id="app">
<h2>{{message}}</h2>
<button @click="message = 'Hello kobe'">点击改变data中的message</button>
<h2>年龄{{age}}</h2>
<button @click="age = 99">点击改变年龄</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
message: "Hello Vue3",
age: 23
}
},
watch: {
message: 'messageMethod', //字符串 结合 methods 侦听
age: [
'handle1',
function handle2(newValue, oldValue) {//数组 结合 methods 侦听
console.log('handle2:', newValue, oldValue)
},
function(newValue,oldValue) {
console.log(this.age)
console.log('匿名函数:', newValue, oldValue)
},
{
handler: function handle3(newValue, oldValue) {
console.log('handle3:', newValue, oldValue)
}
}
]
},
methods: {
messageMethod(newValue, oldValue) {
console.log(newValue, oldValue)
},
handle1(newValue, oldValue) {
console.log('handle1:', newValue, oldValue)
}
}
}).mount("#app")
</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
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
# 12、侦听器watch的其他方式(二)
- 另外一个是Vue3文档中没有提到的,但是Vue2文档中有提到的是侦听对象的属性:
<div id="app">
<h2>{{info}}</h2>
<button @click="info.name = 'james'">按钮</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
info: {
name:'kobe',
age:44
}
}
},
watch: {
'info.name' : function(newValue,oldValue) {//当然我们也可以直接侦听对象里面的属性
console.log()
console.log('侦听info.name:',newValue,oldValue)
}
}
}).mount("#app")
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 还有另外一种方式就是使用 $watch 的API:
- 我们可以在created的生命周期(后续会详细学习)中,使用 this.$watchs 来侦听;
- 第一个参数是要侦听的源;
- 第二个参数是侦听的回调函数callback;
- 第三个参数是额外的其他选项,比如deep、immediate;
<div id="app">
<h2>{{message}}</h2>
<button @click="message = 'kobe'">改变message数据</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
message: "Hello Vue3"
}
},
created() {
this.$watch('message', (newValue, oldValue) => {
console.log('侦听到了message数据发生改变:', newValue, oldValue)
}, { deep: true, immediate: true })
}
}).mount("#app")
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 13、综合案例(购物车)
- 体验网址:https://lyk19990226.github.io/demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
table {
display: inline-block;
border-collapse: collapse;
text-align: center;
}
table caption {
font-weight: 700;
margin-bottom: 8px;
font-size: 20px;
}
table thead {
background-color: rgb(232, 232, 232);
}
table td,
table th {
border: 1px solid rgb(184, 182, 182);
padding: 10px 16px;
}
.books {
display: flex;
margin: 50px 0;
}
.add-book {
width: 620px;
box-sizing: border-box;
margin-left: 150px;
padding: 40px 60px 28px;
border: 2px solid rgb(198, 198, 198);
}
.add-book-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 5px;
text-align: center;
}
.add-book .item {
width: 520px;
}
.add-book .item span {
display: inline-block;
width: 78px;
font-size: 18px;
margin-right: 9px;
color: rgb(98, 97, 97)
}
.add-book .item input {
outline: none;
height: 30px;
width: 200px;
margin: 12px 0 6px;
padding: 2px 8px;
border: 2px solid rgb(8, 8, 8);
border-radius: 5px;
}
.add-book .item-btn input {
background-color: rgb(69, 188, 239);
color: aliceblue;
font-size: 20px;
width: 160px;
height: 40px;
border: none;
cursor: pointer;
box-shadow: 2px 4px 6px 1px rgba(178, 179, 180, 0.8);
}
.add-book .item-btn input:hover {
background-color: rgb(19, 180, 249);
}
.add-book span.tips {
width: auto;
font-size: 16px;
color: red;
margin-left: 12px;
}
.books-shopping,
.books-list {
min-width: 660px;
}
.books-shopping .total {
font-weight: 700;
font-size: 18px;
}
.books-shopping-tips {
color: red;
}
.active {
background-color: pink;
}
</style>
<body>
<div id="app">
<!-- 添加商品,展示商品 -->
<div class="books">
<!-- 商品展示 -->
<table class="books-list">
<caption>书籍列表</caption>
<thead>
<tr>
<th>序号</th>
<th>书籍名称</th>
<th>库存数量</th>
<th>价格</th>
<th>加入购物车</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in books">
<td>{{index}}</td>
<td>{{item.name}}</td>
<td>{{item.inventory}}件</td>
<td>¥{{priceToFixed(item.price)}}</td>
<td>
<button @click="addShopping(item)" :disabled="item.count === item.inventory">加入购物车</button>
</td>
</tr>
</tbody>
</table>
<!-- 添加商品 -->
<div class="add-book">
<div class="add-book-title">添加书籍商品</div>
<div class="item">
<label for="name">
<span>书籍名称:</span>
<input type="text" id="name" placeholder="请输入该商品名称" v-model.lazy="book.name">
<span class="tips" v-show="!book.name.length">{{bookMessage.name}}</span>
</label>
</div>
<div class="item">
<label for="inventory">
<span>库存:</span>
<input type="text" id="inventory" placeholder="请输入该商品库存数量" v-model.lazy="book.inventory">
<span class="tips" v-show="isMessageInventory || !book.inventory.length">{{bookMessage.inventory}}</span>
</label>
</div>
<div class="item">
<label for="date">
<span>出版日期:</span>
<input type="text" id="date" placeholder="请输入该商品出版日期" v-model.lazy="book.date">
<span class="tips" v-show="isMessageDate || !book.date.length">{{bookMessage.date}}</span>
</label>
</div>
<div class="item">
<label for="price">
<span>价格:</span>
<input type="text" id="price" placeholder="请输入该商品价格" v-model.lazy="book.price">
<span class="tips" v-show="isMessagePrice || !book.price.length">{{bookMessage.price}}</span>
</label>
</div>
<div class="item item-btn">
<input type="submit" @click="addBook">
</div>
</div>
</div>
<hr>
<!-- 购物车 -->
<table class="books-shopping">
<caption>购物车</caption>
<thead>
<tr>
<th>序号</th>
<th>书籍名称</th>
<th>出版日期</th>
<th>价格</th>
<th>购买数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in shopping" :key="item.id" :class="{active:index === currentIndex}"
@mouseenter="activeAdd(index)">
<td>{{index+1}}</td>
<td>{{item.name}}</td>
<td>{{item.date}}</td>
<td>¥{{priceToFixed(item.price)}}</td>
<td>
<button @click="increment(index)" :disabled="item.count === 1">-</button>
{{item.count}}
<button @click="decrement(index)" :disabled="item.count === item.inventory">+</button>
</td>
<td>
<button @click="remove(index,item)">移除</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="6" class="books-shopping-tips" v-show="!shopping.length">购物车为空!</td>
</tr>
<tr>
<td class="total" colspan="6">总价:{{total}}</td>
</tr>
</tfoot>
</table>
</div>
<script src="./lib/vue.js"></script>
<script type="text/javascript">
Vue.createApp({
data() {
return {
books: [{
id: 101435435,
name: '《算法导论》',
date: '2006-09',
price: 85.00,
inventory: 201
},
{
id: 101344354,
name: '《UNIX编程艺术》',
date: '2006-02',
price: 59.00,
inventory: 20
},
{
id: 101895688,
name: '《编程珠玑》',
date: '2008-10',
price: 39.00,
inventory: 99
},
{
id: 101793568,
name: '《代码大全》',
date: '2006-03',
price: 128.00,
inventory: 52
}],
shopping: [],
book: {//记录输入框中输入的内容
name: "",
inventory: "",
date: "",
price: ""
},
bookMessage: {//错误提示信息
name: "",
inventory: "",
date: "",
price: ""
},
currentIndex: 0,//购物车中:选中行的样式切换
isMessageInventory: true,
isMessageDate: true,
isMessagePrice: true
}
},
watch: {
'book.inventory': function () {
this.formVerify(/^[0-9]{1,}$/g, this.book.inventory, "请输入数字", 'inventory', 'isMessageInventory')
},
'book.date': {
handler() {
this.formVerify(/^[0-9]{4}\-[0-9]{2}$/g, this.book.date, "请输入日期格式:yyyy-MM", 'date', 'isMessageDate')
}
},
'book.price': function () {
this.formVerify(/^[0-9]{1,}(\.[0-9]{1,})?$/g, this.book.price, "请输入数字", 'price', 'isMessagePrice')
}
},
methods: {
remove(index, item) {
this.shopping.splice(index, 1)
delete item.count
console.log(item)
},
decrement(index) {
this.shopping[index].count++
console.log(this.shopping)
},
increment(index) {
this.shopping[index].count--
},
priceToFixed(value) {
return Number(value).toFixed(2)
},
activeAdd(index) {
this.currentIndex = index
},
addShopping(book) {
if (this.shopping.includes(book)) {
this.shopping[this.shopping.findIndex(item => item.id === book.id)].count++
return
}
book.count = 1
this.shopping.push(book)
console.log(book)
},
addBook() {
const bookKeys = Object.keys(this.book)
const keys = bookKeys.filter(item => this.book[item] === "")
keys.map(key => this.bookMessage[key] = "该为必填项")
// if (!/^[0-9]{1,}$/g.test(this.book.inventory) && this.book.inventory.length) {
// this.bookMessage.inventory = "请输入数字"
// this.isMessageInventory = true
// } else {
// this.isMessageInventory = false
// }
// if (!/^[0-9]{1,}(\.[0-9]{1,})?$/g.test(this.book.price) && this.book.price.length) {
// this.bookMessage.price = "请输入数字"
// this.isMessagePrice = true
// } else {
// this.isMessagePrice = false
// }
// if (!/^[0-9]{4}\-[0-9]{2}$/g.test(this.book.date) && this.book.date.length) {
// this.bookMessage.date = "请输入日期格式:yyyy-MM"
// this.isMessageDate = true
// } else {
// this.isMessageDate = false
// }
// 封装后的代码
// this.formVerify(/^[0-9]{1,}$/g,this.book.inventory,"请输入数字",'inventory','isMessageInventory')
// this.formVerify(/^[0-9]{1,}(\.[0-9]{1,})?$/g, this.book.price, "请输入数字", 'price', 'isMessagePrice')
// this.formVerify(/^[0-9]{4}\-[0-9]{2}$/g, this.book.date, "请输入日期格式:yyyy-MM", 'date', 'isMessageDate')
if (keys.length > 0 || this.isMessageInventory || this.isMessageDate || this.isMessagePrice) return
this.book.id = 101000000 + Math.floor(1000000 * Math.random())
this.book.inventory = Number(this.book.inventory)
this.book.name = `《${this.book.name}》`
this.books.push(JSON.parse(JSON.stringify(this.book)))
bookKeys.map(item => this.book[item] = "")
bookKeys.map(key => this.bookMessage[key] = "")
delete this.book.id
},
formVerify(regexp, value, message, attr, isMessage) {
if (!regexp.test(value) && value.length) {
this.bookMessage[attr] = message
this[isMessage] = true
} else {
this[isMessage] = false
}
}
},
computed: {
total() {
// return this.books.reduce((preValue,curValue) => preValue + curValue.price * curValue.count,0)
if (!this.shopping.length) return 0
return "¥" + this.shopping.map(item => item.price * item.count).reduce((preValue, curValue) => preValue + curValue).toFixed(2)
}
}
}).mount("#app")
</script>
</body>
</html>
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
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# 14、v-model双向数据绑定
# 14.1.v-model的基本使用
- 表单提交是开发中非常常见的功能,也是和用户交互的重要手段:
- 比如用户在登录、注册时需要提交账号密码;
- 比如用户在检索、创建、更新信息时,需要提交一些数据;
- 这些都要求我们可以在代码逻辑中获取到用户提交的数据,我们通常会使用v-model指令来完成:
- v-model指令可以在表单 input、textarea以及select元素上创建双向数据绑定;
- 它会根据控件类型自动选取正确的方法来更新元素;
- 尽管有些神奇,但 v-model 本质上不过是语法糖,它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理;
<template>
<input v-model="searchText">
<h2>{{searchText}}</h2>
</template>
<script>
export default {
data() {
return {
searchText:'nihao v-model'
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 14.2.v-model的原理
- 官方有说到,v-model的原理其实是背后有两个操作:
- v-bind绑定value属性的值;
- v-on绑定input事件监听到函数中,函数会获取最新的值赋值到绑定的属性中;
- 即v-model 是 v-on和v-bind的语法糖写法
<div id="app">
<input type="text" v-model="message">
<h2>{{message}}</h2>
<!-- 等价于 [v-model 是 v-bing 和 v-on 的语法糖写法] -->
<input type="text" :value="message" @input="message = $event.target.value">
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
message: "Hello Vue3"
}
}
}).mount("#app")
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 14.3.事实上v-model更加复杂(源码)
# 15、v-model绑定其他表单类型
# 15.1.v-model绑定textarea
- 我们再来绑定一下其他的表单类型:textarea、checkbox、radio、select
- 我们来看一下绑定textarea:
<div id="app">
<textarea row="30" col="20" v-model="message"></textarea>
<h2>textarea中的内容为:{{message}}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
message:"Hello Vue3"
}
}
}).mount("#app")
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 15.2.v-model绑定checkbox
- 我们来看一下v-model绑定checkbox:单个勾选框和多个勾选框
- 单个复选框:
- v-model即为布尔值。
- 此时input的value属性并不影响v-model的值(点击后始终为布尔值)。【即使你绑定的是非布尔值,点击勾选框后,绑定的值也会变成布尔值】
- 多个复选框:
- 当是多个复选框时,因为可以选中多个【可以将多个复选框绑定到同一个数组或Set (opens new window)值】,所以对应的data中属性是一个数组/Set。
- 当选中某一个时,就会将input的value添加到数组中。
<div id="app">
<!-- 单个勾选框情况 -->
<input type="checkbox" v-model="message">同意协议
<h2>{{message}}</h2>
<!-- 单个勾选框案例 -->
<h4>案例:isShow的值:{{isShow}}</h4>
<label for="agreement" style="user-select:none;">
<input type="checkbox" v-model="isShow" id="agreement">同意协议
</label>
<button v-show="isShow">下一步</button>
<hr>
<!-- 多个复选框情况 -->
<form action="http://lyk.com/">
<label for="sing">
<input type="checkbox" value="sing" v-model="hobbies" name="hobbies" id="sing">唱
</label>
<label for="jump">
<input type="checkbox" value="jump" v-model="hobbies" name="" id="jump">跳
</label>
<label for="rap">
<input type="checkbox" value="rap" v-model="hobbies" name="" id="rap">rap
</label>
<label for="basketball">
<input type="checkbox" value="basketball" v-model="hobbies" name="" id="basketball">篮球
</label>
<h2>hobbies当前的值是:{{hobbies}}</h2>
<input type="submit">
</form>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
message: "Hello Vue3",
isShow: false,
hobbies: ['sing']
}
}
}).mount("#app")
</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
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
# 15.3.v-model绑定radio
- v-model绑定radio,用于选择其中一项;
<div id="app">
<form action="https://lyk.com">
<label for="male">
<input type="radio" v-model="gender" id="male" value="male">男
</label>
<label for="female">
<input type="radio" v-model="gender" id="female" value="female">女
</label>
<input type="submit">
</form>
<h2>当前选中的性别是:{{gender}}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
gender:'male'
}
}
}).mount("#app")
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 15.4.v-model绑定select
- 和checkbox一样,select也分单选和多选两种情况。
- 单选:只能选中一个值
- v-model绑定的是一个值;
- 当我们选中option中的一个时,会将它对应的value赋值到fruit中;
- 多选:可以选中多个值
- v-model绑定的是一个数组;
- 当选中多个值时,就会将选中的option对应的value添加到数组fruit中;
<div id="app">
<!-- 单选的情况 -->
<form action="http://lyk.com">
<select name="fruit" v-model="fruit">
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
<input type="submit">
</form>
<h2>当前选中的水果是:{{fruit}}</h2><hr>
<!-- 多选的情况,按住ctrl 点击即可多选 -->
<form action="http://lyk/demo.com">
<select name="hobbies" multiple v-model="hobbies">
<option value="sing">唱</option>
<option value="jump">跳</option>
<option value="rap">rap</option>
<option value="basketball">basketball</option>
</select>
<input type="submit">
</form>
<h2>当前选中的爱好有:{{hobbies}}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
fruit:'orange',
hobbies:['sing','rap']
}
}
}).mount("#app")
</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
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
# 15.5.v-model的值绑定
- 目前我们在前面的案例中大部分的值都是在template中固定好的:
- 比如gender的两个输入框值male、female;
- 比如hobbies的三个输入框值basketball、football、tennis;
- 在真实开发中,我们的数据可能是来自服务器的,那么我们就可以先将值请求下来,绑定到data返回的对象中,再通过v-bind来进行值的绑定,这个过程就是值绑定。
- 这里不再给出具体的做法,因为还是v-bind的使用过程。
# 16、v-model的修饰符
# 16.1.v-model修饰符 - lazy
- lazy修饰符是什么作用呢?
- 默认情况下,v-model在进行双向绑定时,绑定的是input事件,那么会在每次内容输入后就将最新的值和绑定的属性进行同步;
- 如果我们在v-model后跟上lazy修饰符,那么会将绑定的事件切换为 change 事件,只有在提交时(比如回车,input失去焦点)才会触发;
<div id="app">
<!-- 1.常见修饰符一:lazy 失去焦点才触发-->
<input type="text" v-model.lazy="message">
<h2>{{message}}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
message: "hello model.lazy",
}
},
}).mount("#app")
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 16.2.v-model修饰符 - number
- 我们先来看一下v-model绑定后的值是什么类型的:
- message总是string类型:即使在我们设置type为number也是string类型;
<template>
<!-- 类型 -->
<input type="text" v-model="message"><br>
<input type="number" v-model="message">{{typeof message}}类型
<h2>{{message}}</h2>
</template>
<script>
export default {
data() {
return {
message:'hello model'
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 如果我们希望转换为数字类型,那么可以使用 .number 修饰符:
<div id="app">
<!-- 3.常用的修饰符三:number -->
<input type="text" v-model="score">{{typeof score}}类型<br>
<input type="text" v-model.number="score1">{{typeof score1}}类型<br>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
score:"100",
score1:"100"
}
},
watch: {
score(newScore) {
console.log(typeof this.score,this.score)
},
score1(newScore1) {
console.log(typeof this.score1,this.score1)
},
}
}).mount("#app")
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 另外,在我们进行逻辑判断时,如果是一个string类型,在可以转化的情况下会进行隐式转换的:
- 下面的score在进行判断的过程中会进行隐式转化的;
const score = "100"
if(score > 90) {
console.log('优秀了')
}
console.log(Number("100"))//100
console.log(typeof score)
1
2
3
4
5
6
2
3
4
5
6
# 16.3.v-model修饰符 - trim
- 如果要自动过滤用户输入的首尾空白字符,可以给v-model添加 trim 修饰符
<div id="app">
<!-- 2.常见的修饰符二:trim 去除首尾空格 -->
<input type="text" v-model.trim="message">
<h2>{{message}}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
message: " Hello model.trim "
}
},
watch: {
message: {
handler(newMessage, oldMessage) {
console.log(newMessage,newMessage.length)
},
immediate:true
}
}
}).mount("#app")
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 17、v-model组件上使用
# 17.1.组件的v-model
v-model也可以使用在组件上,Vue2版本和Vue3版本有一些区别。
- 具体的使用方法,(推荐学习完vue组件化开发再来这里学习v-model组件上使用方法)
前面我们在input中可以使用v-model来完成双向绑定:
- 这个时候往往会非常方便,因为v-model默认帮助我们完成了两件事;
- v-bind:value的数据绑定和@input的事件监听;
如果我们现在封装了一个组件,其他地方在使用这个组件时,是否也可以使用v-model来同时完成这两个功能呢?
- 也是可以的,vue也支持在组件上使用v-model;
当我们在组件上使用的时候,等价于如下的操作:
- 我们会发现和input元素不同的只是属性的名称和事件触发的名称而已;
# 17.2.组件v-model的实现
- 那么,为了我们的MyInput组件可以正常的工作,这个组件内的 必须:
- 将其 value attribute 绑定到一个名叫 modelValue 的 prop 上;
- 在其 input 事件被触发时,将新的值通过自定义的 update:modelValue 事件抛出;
- MyInput.vue的组件代码如下:
# 17.3.绑定多个属性
- 我们现在通过v-model是直接绑定了一个属性,如果我们希望绑定多个属性呢?
- 也就是我们希望在一个组件上使用多个v-model是否可以实现呢?
- 我们知道,默认情况下的v-model其实是绑定了 modelValue 属性和 @update:modelValue的事件;
- 如果我们希望绑定更多,可以给v-model传入一个参数,那么这个参数的名称就是我们绑定属性的名称;
- 注意:这里我是绑定了两个属性的
<template>
<my-input v-model="message" v-model:title="tilte"/>
</template>
1
2
3
2
3
- v-model:title相当于做了两件事:
- 绑定了title属性;
- 监听了 @update:title的事件;