Vue学习笔记02-模板语法

双括号,v-once,v-text,v-html,v-pre,v-cloak,v-bind,v-on,v-model,computed,watch

1. 模板语法

  • React的开发模式:

    • React使用的jsx,所以对应的代码都是编写的类似于js的一种语法;
    • 之后通过Babel将jsx编译成 React.createElement 函数调用;
  • Vue也支持jsx的开发模式):

    • 但是大多数情况下,使用基于HTML的模板语法;
    • 在模板中,允许开发者以声明式的方式将DOM底层组件实例的数据绑定在一起;
    • 在底层的实现中,Vue将模板编译成虚拟DOM渲染函数;

2. Mustache双大括号语法

  • 如果希望把数据显示到模板(template)中,使用最多的语法是 “Mustache”语法 (双大括号) 的文本插值。
    • 并且data返回的对象是有添加到Vue的响应式系统中;
    • 当data中的数据发生改变时,对应的内容也会发生更新。
    • 当然,Mustache中不仅仅可以是data中的属性,也可以是一个JavaScript的表达式。

正确写法

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
<body>
<div id="app"></div>
<template id = "vue">
<!-- mustanche基本使用 -->
<h2>{{message}}</h2>
<!-- javascript表达式 -->
<h2>{{counter*2}}</h2>
<h2>{{message.split(" ").reverse().join(" ")}}</h2>
<!-- 调用一个methods中的函数 -->
<h2>{{getMessage(message)}}</h2>
<!-- 三元运算符 -->
<h2>{{flag? '1':'0'}}</h2>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
message: 'Hello Vue',
counter: 10,
flag: true
}
},
methods: {
getMessage(msg){
return msg.split(" ").reverse().join("-");
}
}
}).mount("#app");
</script>
</body>

错误写法

1
2
3
4
5
6
7
8
<!-- 错误写法 -->
<!-- Uncaught SyntaxError: Unexpected token 'var' -->
<!-- 这是一个赋值语句 -->
<!-- <h2>{{var time = 1}}</h2> -->

<!-- Uncaught SyntaxError: Unexpected token 'if' -->
<!-- 控制流的if语句也是不支持的,可以使用三元运算符 -->
<!-- <h2>{{if(true){ return '1';} }}</h2> -->

3. v-once

  • v-once用于指定元素或者组件只渲染一次:

    • 当数据发生变化时,元素或者组件以及其所有的子元素将视为静态内容并且跳过;

    • 该指令可以用于性能优化;

      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
      <body>
      <div id="app"></div>
      <template id="vue">
      <div>
      <h2 v-once>current:{{counter}}</h2>
      <h2>{{counter}}</h2>
      <button @click = 'increment'>+1</button>
      </div>
      </template>
      <script src="https://unpkg.com/vue@next"></script>
      <script>
      Vue.createApp({
      template: '#vue',
      data: function(){
      return {
      counter: 10,
      }
      },
      methods: {
      increment(){
      this.counter++;
      }
      }
      }).mount("#app");
      </script>
      </body>
  • 如果是子节点,也是只会渲染一次:

    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
    <body>
    <div id="app"></div>
    <template id="vue">
    <div v-once>
    <h2>current:{{counter}}</h2>
    <h2>{{counter}}</h2>
    <button @click = 'increment'>+1</button>
    </div>
    </template>
    <script src="https://unpkg.com/vue@next"></script>
    <script>
    Vue.createApp({
    template: '#vue',
    data: function(){
    return {
    counter: 10,
    }
    },
    methods: {
    increment(){
    this.counter++;
    }
    }
    }).mount("#app");
    </script>
    </body>

4. v-text

用于更新元素的 textContent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app"></div>
<template id="vue">
<div>
<h2 v-text="counter"></h2>
<!-- 两者相同 -->
<h2>{{counter}}</h2>
</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
counter: 10,
}
}
}).mount("#app");
</script>
</body>

5. v-html

  • 默认情况下,如果展示的内容本身是 html 的,那么vue并不会对其进行特殊的解析。
    • 如果希望这个内容被Vue可以解析出来,那么可以使用 v-html 来展示;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body>
<div id="app"></div>
<template id="vue">
<div>
<h2>{{info}}</h2>
<h2 v-html="info"></h2>
</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
info: '<h2>Hello</h2>'
}
}
}).mount("#app");
</script>
</body>

6. v-pre

  • 用于跳过元素和它的子元素的编译过程,显示原始的Mustache标签:
    • 跳过不需要编译的节点,加快编译的速度;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<div id="app"></div>
<template id="vue">
<div>
<h2 v-pre>{{info}}</h2>
</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
info: '<h2>Hello</h2>'
}
}
}).mount("#app");
</script>
</body>

7. v-cloak

  • 这个指令保持在元素上直到关联组件实例结束编译

    • 和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到组件实例准备完毕
  • <div> 不会显示,直到编译结束

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
<!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>
<style>
[v-cloak]{
display: none;
}
</style>
</head>
<body>
<div id="app"></div>
<template id="vue">
<div v-cloak>
<h2>{{message}}</h2>
</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
message: 'Hello'
}
}
}).mount("#app");
</script>
</body>
</html>

8. v-bind的绑定属性

  • 前端讲的一系列指令,主要是将值插入到模板内容中。

  • 但是,除了内容需要动态来决定外,某些属性也希望动态来绑定

    • 比如动态绑定a元素的href属性;
    • 比如动态绑定img元素的src属性;
  • 绑定属性使用v-bind

    • 缩写 ::
    • 预期 :any (with argument) | Object (without argument)
    • 参数 :attrOrProp (optional)
    • 修饰符:
      • .camel - 将 kebab-case attribute 名转换为 camelCase
  • 用法:动态地绑定一个或多个 attribute,或一个组件 prop 到表达式

8.1. 绑定基本属性

  • v-bind用于绑定一个或多个属性值,或者向另一个组件传递props值
  • 在开发中,有哪些属性需要动态进行绑定呢?
    • 比如图片的链接src、网站的链接href、动态绑定一些类、样式等等
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
<body>
<div id="app"></div>
<template id="vue">
<div>
<!-- 基本使用 -->
<a v-bind:href="href">跳转</a>
<img v-bind:src="imgUrl" alt="">

<!-- 语法糖 -->
<a :href="href">链接</a>
<img :src="imgUrl" alt="">
</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
href: 'https://www.baidu.com',
imgUrl: './photo.jpg'
}
}
}).mount("#app");
</script>
</body>

8.2. 绑定class

  • 在开发中,元素class也是动态的,比如:

    • 当数据为某个状态时,字体显示红色。
    • 当数据另一个状态时,字体显示黑色。
  • 绑定class有两种方式:

    • 对象语法
    • 数组语法

8.2.1. 对象语法

传给 :class (v-bind:class 的简写) 一个对象,以动态地切换 class

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
<!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>
<style>
div{
width: 200px;
height: 200px;
}
.active{
background-color: red;
}
</style>
</head>
<body>
<div id="app"></div>
<template id="vue">
<div>
<!-- 1. 普通绑定方式 -->
<div :class = "className"></div>

<!-- 2. 对象语法 -->
<!-- 动态切换class是否加入{类变量:boolean} -->
<div :class="{'active':isActive}"></div>
<button @click="toggle">转换</button>

<!-- 多个键值对 -->
<div :class="{active : isActive, title : true}"></div>

<!-- 3. 绑定对象 -->
<div :class = "classObj"></div>

<!-- 4. 从methods中获取 -->
<div :class="getClassObj()"></div>
</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
className: 'active',
isActive: false,
classObj :{
active: true,
title: true
}
}
},
methods: {
toggle(){
this.isActive = !this.isActive;
},
getClassObj(){
return {
active: true,
title: true
}
}
}
}).mount("#app");
</script>
</body>
</html>

8.2.2. 数组语法

把一个数组传给 :class,以应用一个 class 列表;

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
<!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>
<style>
div{
width: 200px;
height: 200px;
}
.active{
background-color: red;
}
.black{
border: 2px solid black;
}
</style>
</head>
<body>
<div id="app"></div>
<template id="vue">
<div>
<!-- 1. 直接传入一个数组 -->
<div :class = "['class1',className]"></div>

<!-- 2. 使用三元运算符-->
<!-- 动态切换class是否加入{类变量:boolean} -->
<div :class="['class1',className,isActive? 'active':'']"></div>

<!-- 数组中使用对象语法 -->
<div :class="['class1',className,{'active':isActive}]"></div>


</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
className: 'black',
isActive: true
}
}
}).mount("#app");
</script>
</body>
</html>

8.3. 绑定style

  • 利用 v-bind:style来绑定一些CSS内联样式:

    • 因为某些样式需要根据数据动态来决定;
    • 比如某段文字的颜色,大小等等;
  • CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名;

  • 绑定class有两种方式:

    • 对象语法
    • 数组语法

8.3.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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!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>
<style>
div{
width: 150px;
height: 50px;
}
</style>
</head>
<body>
<div id="app"></div>
<template id="vue">
<div>
<!-- 1. 传入一个对象,并且对象内容都是确定的 -->
<div :style = "{color:'red', fontSize: '30px'}">{{message}}</div>

<!-- 2. 变量数据:传入一个对象,值来自于data -->
<div :style="{color:'yellow', fontSize: size+'px'}">{{message}}</div>

<!-- 3. 对象数据:直接在data中定义好对象在这里使用 -->
<div :style = "styleObj">{{message}}</div>

<!-- 4. 对象数据:直接在methods中定义好对象在这里使用 -->
<div :style = "getStyleObj()">{{message}}</div>

</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
message: 'Hello style',
size: 20
}
},
methods: {

styleObj(){
return {
color: 'purple',
fontSize: '15px'
}
}
}
}).mount("#app");
</script>
</body>
</html>

8.3.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!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>
<style>
div{
width: 150px;
height: 50px;
}
</style>
</head>
<body>
<div id="app"></div>
<template id="vue">
<div>
<!-- 数组 -->
<div :style="[styleObj1,styleObj2]">{{message}} - array</div>

</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
message: 'Hello style',
styleObj1:{
'border-bottom': '1px solid #354',
'text-align': 'center'
},
styleObj2: {
'border-top': '1px solid #354'
}
}
}
}).mount("#app");
</script>
</body>
</html>

8.4. 动态绑定属性

  • 在某些情况下,属性的名称可能也不是固定的:
    • 无论绑定src、href、class、style,属性名称都是固定的;
    • 如果属性名称不是固定的,可以使用 :[属性名]="值" 的格式来定义;
    • 这种绑定的方式,称之为动态绑定属性;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<div id="app"></div>
<template id="vue">
<div :[name]="value">
{{message}}
</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
message: 'Hello',
name: 'style',
value: {
color: 'red'
}
}
}
}).mount("#app");
</script>
</body>

8.5. 绑定对象

  • 如果希望将一个对象的所有属性,绑定到元素上的所有属性,应该怎么做呢?

    • 非常简单,可以直接使用 v-bind 绑定一个对象;
  • 案例:info对象会被拆解成div的各个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<div id="app"></div>
<template id="vue">
<div v-bind="info">
{{message}}
</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
message: 'Hello',
info: {
name: 'mac',
age: 17,
height: 1.8
}
}
}
}).mount("#app");
</script>
</body>

得到结果

1
<div name="mac" age="17" height="1.8">Hello</div>

9. v-on

  • 在前端开发中,需要经常和用户进行各种各样的交互:
    • 监听用户发生的事件,比如点击、拖拽、键盘事件等等
    • 在Vue中如何监听事件呢? 使用v-on指令

9.1. v-on的用法

  • v-on的使用:
    • 缩写:@
    • 预期:Function | Inline Statement | Object
    • 参数:event
    • 修饰符:
      • .stop - 调用 event.stopPropagation()。
      • .prevent - 调用 event.preventDefault()
      • .capture - 添加事件侦听器时使用 capture 模式
      • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调
      • .{keyAlias} - 仅当事件是从特定键触发时才触发回调
      • .once - 只触发一次回调
      • .left - 只当点击鼠标左键时触发
      • .right - 只当点击鼠标右键时触发
      • .middle - 只当点击鼠标中键时触发
      • .passive - { passive: true } 模式添加侦听器
    • 用法:绑定事件监听

9.2. v-on的基本使用

  • 使用v-on来监听一下点击的事件
  • v-on:click可以写成@click,是它的语法糖写法
  • 也可以绑定其他的事件
  • 如果希望一个元素绑定多个事件,可以传入一个对象
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
<!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>
<style>
.active{
width: 200px;
height: 100px;
background-color: pink;
margin: 10px;
}
</style>
</head>
<body>
<div id="app"></div>
<template id="vue">
<h2>{{counter}}</h2>
<!-- 1. 基本使用 -->
<!-- 绑定一个表达式 -->
<button v-on:click="counter++">绑定表达式</button>
<!-- 绑定到一个methods方法 -->
<button v-on:click = "btnClick">绑定methods方法</button>

<!-- v-on 语法糖 -->
<button @click="btnClick">语法糖</button>

<!-- 绑定鼠标事件 -->
<div class="active" @mousemove="mouseMove">鼠标事件</div>

<!-- 一个元素绑定多个事件 -->
<div class="active" v-on="{click:btnClick, mousemove:mouseMove}">绑定多个事件</div>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
counter: 10
}
},
methods: {
btnClick(){
this.counter--;
},
mouseMove(){
console.log("div区域");
}
}
}).mount("#app");
</script>
</body>
</html>

9.3. v-on参数传递

  • 当通过methods中定义方法,以供@click调用时,需要注意参数问题:
  • 情况一:如果该方法不需要额外参数,那么方法后的()可以不添加
    • 但是注意:如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去
  • 情况二:如果需要同时传入某个参数,同时需要event时,可以通过$event传入事件
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
<body>
<div id="app"></div>
<template id="vue">
<!-- 默认会把event对象传入-->
<button @click="btnClick">默认</button>
<!-- 可以传入其他属性 -->
<button @click = "btnClick2($event,'vue')">传入多个参数</button>

</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
methods: {
btnClick(event){
// PointerEvent {isTrusted: true, pointerId: 1, width: 1, height: 1, pressure: 0, …}
console.log(event);
},
btnClick2(event,name){
console.log(event,name);
}
}
}).mount("#app");
</script>
</body>

9.4. v-on的修饰符

v-on支持修饰符,修饰符相当于对事件进行了一些特殊的处理

  • .stop - 调用 event.stopPropagation()。
  • .prevent - 调用 event.preventDefault()
  • .capture - 添加事件侦听器时使用 capture 模式
  • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调
  • .{keyAlias} - 仅当事件是从特定键触发时才触发回调
  • .once - 只触发一次回调
  • .left - 只当点击鼠标左键时触发
  • .right - 只当点击鼠标右键时触发
  • .middle - 只当点击鼠标中键时触发
  • .passive - { passive: true } 模式添加侦听器
1
2
3
4
5
6
<template id="vue">
<!-- 禁止冒泡 -->
<div>
<button @click.stop="btnClick">默认</button>
</div>
</template>

10. 条件渲染

  • 在某些情况下,需要根据当前的条件决定某些元素或组件是否渲染,这个时候我们就需要进行条件判断了。

  • Vue提供了下面的指令来进行条件判断:

    • v-if
    • v-else
    • v-else-if
    • v-show

10.1. v-if、v-else、v-else-if

  • v-if、v-else、v-else-if 用于根据条件来渲染某一块的内容:
    • 这些内容只有在条件为true时,才会被渲染出来;
    • 这三个指令与JavaScript的条件语句if、else、else if类似;
  • v-if的渲染原理:
    • v-if是惰性的;
    • 当条件为false时,其判断的内容完全不会被渲染或者会被销毁掉;
    • 当条件为true时,才会真正渲染条件块中的内容;
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
<!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>
<body>
<div id="app"></div>
<template id="vue">
<input type="text" v-model.number="score">
<h2 v-if="score>90">优秀</h2>
<h2 v-else-if="score>80">良好</h2>
<h2 v-else-if="score>60">及格</h2>
<h2 v-else>不及格</h2>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
score: 90
}
}
}).mount("#app");
</script>
</body>
</html>

10.2. template元素

  • 因为v-if是一个指令,所以必须将其添加到一个元素上:

    • 但是如果希望切换的是多个元素呢?
    • 此时渲染div,但是并不希望div这种元素被渲染;
    • 这个时候,可以选择使用template;
  • template元素可以当做不可见的包裹元素,并且在v-if上使用,但是最终template不会被渲染出来:

    • 有点类似于小程序中的block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<div id="app"></div>
<template id="vue">
<template v-if="isShow">
<h2>哈哈哈</h2>
</template>
<template v-else>
<h2>呵呵呵</h2>
</template>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
isShow: false
}
}
}).mount("#app");
</script>
</body>

10.3. v-show

v-show和v-if的用法看起来是一致的,也是根据一个条件决定是否显示元素或者组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<div id="app"></div>
<template id="vue">
<h2 v-show="isShow">哈哈哈</h2>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
isShow: false
}
}
}).mount("#app");
</script>
</body>

v-show 修饰的元素表现为 <h2 style="display: none;">哈哈哈</h2>

10.4. v-show和v-if的区别

  • 首先,在用法上的区别:

    • v-show是不支持template;
    • v-show不可以和v-else一起使用;
  • 其次,本质的区别:

    • v-show元素无论是否需要显示到浏览器上,它的DOM实际都是有渲染的,只是通过CSS的display属性来进行切换;
    • v-if当条件为false时,其对应的原生压根不会被渲染到DOM中;
  • 开发中如何进行选择呢?

    • 如果原生需要在显示和隐藏之间频繁的切换,那么使用v-show;
    • 如果不会频繁的发生切换,那么使用v-if;

11. 列表渲染

  • 在真实开发中,往往会从服务器拿到一组数据,并且需要对其进行渲染。
    • 这个时候可以使用v-for来完成;
    • v-for类似于JavaScript的for循环,可以用于遍历一组数据;

11.1. v-for基本使用

  • v-for的基本格式是 item in 数组:

    • 数组通常是来自data或者prop,也可以是其他方式;

    • item是给每项元素起的一个别名,这个别名可以自定来定义;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      <body>
      <div id="app"></div>
      <template id="vue">
      <h2 v-for="item in movies">{{item}}</h2>
      </template>
      <script src="https://unpkg.com/vue@next"></script>
      <script>
      Vue.createApp({
      template: '#vue',
      data: function(){
      return {
      movies: [
      "战狼",
      "盗梦空间",
      "当幸福来敲门"
      ]
      }
      }
      }).mount("#app");
      </script>
      </body>
  • 在遍历一个数组的时候会经常需要拿到数组的索引:

    • 如果需要索引,可以使用格式: “(item, index) in 数组”;

    • 注意上面的顺序:数组元素项item是在前面的,索引项index是在后面的;

      1
      <h2 v-for="(item,index) in movies">{{index+1}}-{{item}}</h2>

11.2. v-for支持的类型

  • v-for 支持遍历对象,并且支持有一二三个参数:

    • 一个参数: “value in object”;

    • 二个参数: “(value, key) in object”;

    • 三个参数: “(value, key, index) in object”;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      <body>
      <div id="app"></div>
      <template id="vue">

      <!-- 对象 key-value -->
      <h2 v-for="(value,key,index) in info">{{index}}-{{key}}-{{value}}</h2>

      </template>
      <script src="https://unpkg.com/vue@next"></script>
      <script>
      Vue.createApp({
      template: '#vue',
      data: function(){
      return {
      info: {
      name: 'uname',
      age: 18,
      height: 1.8
      }
      }
      }
      }).mount("#app");
      </script>
      </body>
  • v -for同时也支持数字的遍历:

    • 每一个item都是一个数字;

      1
      2
      <!-- 遍历数字 1-10 -->
      <h2 v-for="item in 10">{{item}}</h2>

11.3. template元素

  • 类似于v-if,使用 template 元素来循环渲染一段包含多个元素的内容:
    • 使用template来对多个元素进行包裹,而不是使用div来完成;
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
<body>
<div id="app"></div>
<template id="vue">

<!-- template -->
<ul>
<template v-for="(value,key) in info">
<li>{{key}}</li>
<li>{{value}}</li>
<hr>
</template>
</ul>


</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
info: {
name: 'uname',
age: 18,
height: 1.8
}
}
}
}).mount("#app");
</script>
</body>

11.4. 数组更新检测

  • Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

    • push():添加
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()
  • 替换数组的方法

    • 上面的方法会直接修改原来的数组,但是某些方法不会替换原来的数组,而是会生成新的数组,比如 filter()、 concat() 和 slice()
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
<body>
<div id="app"></div>
<template id="vue">
<ul>
<li v-for="(value,index) in movies">
{{index}}-{{value}}
</li>
</ul>
<input type="text" v-model="txt">
<button @click="addMovie">添加</button>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
movies: [
"战狼",
"盗梦空间",
"当幸福来敲门"
],
txt: ""
}
},
methods: {
addMovie(){
this.movies.push(this.txt);
this.txt = "";
}
}
}).mount("#app");
</script>
</body>

11.5. v-for中的key是什么作用?

  • 在使用v-for进行列表渲染时,通常会给元素或者组件绑定一个 key属性

  • 这个key属性有什么作用呢?

    • https://cn.vuejs.org/v2/api/#key

    • key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes;

    • 如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法;

    • 使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素;

  • 官方的解释对于初学者来说并不好理解,比如下面的问题:

    • 什么是新旧nodes,什么是VNode?
    • 没有key的时候,如何尝试修改和复用的?
    • 有key的时候,如何基于key重新排列的?

11.6. 认识VNode

  • 先来解释一下VNode的概念:
    • 先理解HTML元素创建出来的VNode;
    • VNode的全称是Virtual Node,也就是虚拟节点;
    • 事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode;
    • VNode的本质是一个JavaScript的对象;

流程图为:template -> VNode -> 真实DOM

1
<div class="title" style="font-size:30px; color:red;">哈哈哈</div>

对应 VNode

1
2
3
4
5
6
7
8
9
10
11
const vnode = {
type: "div",
props: {
class: "title",
style: {
"font-size": "30px",
color: "red"
}
},
children: "哈哈哈"
}

11.7. 虚拟DOM

如果不只是一个简单的div,而是有一大堆的元素,那么它们应该会形成一个VNode Tree:

11.8. 插入F的案例

先来看一个案例—这个案例是当点击按钮时会在中间插入一个f;

  • 可以确定的是,这次更新对于ul和button是不需要进行更新,需要更新的是li的列表:
    • 在Vue中,对于相同父元素的子元素节点并不会重新渲染整个列表;
    • 因为对于列表中 a、b、c、d 它们都是没有变化的;
    • 在操作真实DOM的时候,我们只需要在中间插入一个f的li即可;
  • 那么Vue中对于列表的更新究竟是如何操作的呢?
    • Vue事实上会对于有key和没有key会调用两个不同的方法;
    • 有key,那么就使用 patchKeyedChildren方法;
    • 没有key,那么久使用 patchUnkeyedChildren方法;
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
<body>
<div id="app"></div>
<template id="vue">
<ul>
<li v-for="item in letters">{{item}}</li>
</ul>
<button @click="insertF">插入 f</button>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
letters: ["a","b","c","d"]
}
},
methods: {
insertF(){
/**
* array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
* start
* 指定修改的开始位置(从 0 计数)
* deleteCount
* 整数,表示要移除的数组元素的个数
* item1, item2, ...
* 要添加进数组的元素,从start 位置开始
*/
this.letters.splice(2,0,'f');
}
}
}).mount("#app");
</script>
</body>

11.9. Vue源码对于key的判断

packages/runtime-core/src/renderer.ts

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
// 如果有key
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
// 如果没key
else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}

11.9.1. 没有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
const patchUnkeyedChildren = (
// 旧 VNode
c1: VNode[],
// 新 VNode
c2: VNodeArrayChildren
) => {
// 获取旧节点的长度
const oldLength = c1.length
// 获取新节点的长度
const newLength = c2.length
// 获取最小的长度值
const commonLength = Math.min(oldLength, newLength)
let i
// 从 0 位置开始依次patch比较
for (i = 0; i < commonLength; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch(
c1[i],
nextChild
)
}
// 如果旧节点数大于新节点数
if (oldLength > newLength) {
// 移除剩余的节点
unmountChildren(
c1
)
}
else {
// 创建新节点
mountChildren(
c2
)
}
}
  • 会发现上面的diff算法效率并不高:
    • c和d来说事实上并不需要有任何的改动;
    • 但是因为c被f所使用了,所有后续所有的内容都要一次进行改动,并且最后进行新增;

11.9.2. 有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
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
// can be all-keyed or mixed
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren
) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index

// 1. 从头部开始遍历,遇到相同的节点就继续,遇到不同的就跳出循环
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = c2[i]
if (isSameVNodeType(n1, n2)) {
patch()
}
else {
break
}
i++
}

// 2. 从尾部开始遍历,遇到相同的节点就继续,遇到不同的就跳出循环
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
if (isSameVNodeType(n1, n2)) {
patch()
}
else {
break
}
e1--
e2--
}

// 3. 如果旧节点遍历完了,依然有新的节点,那么就添加新节点
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch()
i++
}
}
}

// 4. 如果新的节点遍历完了,还有旧节点,那么就移除旧节点
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}

// 5. 如果中间存在不知道如何排列的位置序列,
// 如果就使用key建立索引图,最大限度的使用旧节点
else {
}
}
  1. 第一步的操作是从头开始进行遍历、比较:

    • a和b是一致的会继续进行比较;

    • c和f因为key不一致,所以就会break跳出循环;

  2. 第二步的操作是从尾部开始进行遍历、比较:

  3. 第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:

  4. 第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点:

  5. 第五步是最特色的情况,中间还有很多未知的或者乱序的节点:

  6. 可以发现,Vue在进行diff算法的时候,会尽量利用key来进行优化操作:

    • 在没有key的时候效率是非常低效的;
    • 在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加的高效;

12. 计算属性 computed

12.1. 复杂data的处理方式

  • 在模板中可以直接通过插值语法显示一些data中的数据

  • 但是在某些情况,可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示;

    • 比如需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示;
    • 在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算;
    • 在模板中放入太多的逻辑会让模板过重和难以维护;
    • 并且如果多个地方都使用到,那么会有大量重复的代码;
  • 有没有什么方法可以将逻辑抽离出去呢?

  • 可以,其中一种方式就是将逻辑抽取到一个method中,放到methods的options中;

  • 但是,这种做法有一个直观的弊端,就是所有的data使用过程都会变成了一个方法的调用;

  • 另外一种方式就是使用计算属性computed;

12.2. 认识计算属性computed

  • 什么是计算属性呢?

    • 官方并没有给出直接的概念解释;
    • 而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性;
    • 计算属性将被混入到组件实例中。
    • 所有 getter 和 setter 的 this 上下文自动地绑定为组件实例;
  • 计算属性的用法:

  • 选项:computed

  • 类型:{ [key: string]: Function | { get: Function, set: Function } }

12.3. 案例实现思路

  1. 案例一:两个变量:firstName和lastName,希望它们拼接之后在界面上显示;
  2. 案例二:有一个分数:score
    • 当score大于60的时候,在界面上显示及格;
    • 当score小于60的时候,在界面上显示不及格;
  3. 案例三:有一个变量message,记录一段文字:比如Hello World
    • 某些情况下我们是直接显示这段文字;
    • 某些情况下我们需要对这段文字进行反转;
  • 有三种实现思路:
    1. 思路一:在模板语法中直接使用表达式;
    2. 思路二:使用method对逻辑进行抽取;
    3. 思路三:使用计算属性computed;

12.3.1. 实现思路一:模板语法

  • 缺点一:模板中存在大量的复杂逻辑,不便于维护(模板中表达式的初衷是用于简单的计算);
  • 缺点二:当有多次一样的逻辑时,存在重复的代码;
  • 缺点三:多次使用的时候,很多运算也需要多次执行,没有缓存;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<div id="app"></div>
<template id="vue">
<h2>{{firstName + lastName}}</h2>
<h2>{{score>60?'及格':'不及格'}}</h2>
<h2>{{message.split("").reverse().join("")}}</h2>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
firstName: 'John',
lastName: 'H',
message: 'Hello World',
score: 80
}
}
}).mount("#app");
</script>
</body>

12.3.2. 实现思路二:method实现

  • 缺点一:事实上先显示的是一个结果,但是都变成了一种方法的调用;
  • 缺点二:多次使用方法的时候,没有缓存,也需要多次计算;
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
<body>
<div id="app"></div>
<template id="vue">
<h2>{{getName()}}</h2>
<h2>{{getScore()}}</h2>
<h2>{{getMessage()}}</h2>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
firstName: 'John',
lastName: 'H',
message: 'Hello World',
score: 80
}
},
methods: {
getName(){
return this.firstName + this.lastName;
},
getScore(){
return this.score>60?'及格':'不及格';
},
getMessage(){
return this.message.split("").reverse().join("");
}
}
}).mount("#app");
</script>
</body>

12.3.3. 实现思路三:computed实现

  • 注意:计算属性看起来像是一个函数,但是在使用的时候不需要加();
  • 无论是直观上,还是效果上计算属性都是更好的选择;
  • 并且计算属性是有缓存的;
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
<body>
<div id="app"></div>
<template id="vue">
<h2>{{getName}}</h2>
<h2>{{getScore}}</h2>
<h2>{{getMessage}}</h2>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
firstName: 'John',
lastName: 'H',
message: 'Hello World',
score: 80
}
},
computed: {
getName(){
return this.firstName + this.lastName;
},
getScore(){
return this.score>60?'及格':'不及格';
},
getMessage(){
return this.message.split("").reverse().join("");
}
}
}).mount("#app");
</script>
</body>

12.3.4. 计算属性 vs methods

  • 计算属性和methods的实现看起来是差别是不大的,而且 计算属性有缓存
  • 同一个计算多次使用,计算属性和methods的差异:
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
<body>
<div id="app"></div>
<template id="vue">
<h2>{{result}}</h2>
<h2>{{result}}</h2>
<h2>{{result}}</h2>

<h2>{{getScore()}}</h2>
<h2>{{getScore()}}</h2>
<h2>{{getScore()}}</h2>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
score: 80
}
},
methods: {
getScore(){
console.log("调用了getScore方法");
return this.score>60?'及格':'不及格';
}
},
computed: {
result(){
console.log("调用了计算属性result的getter");
return this.score>60?'及格':'不及格';
}
}
}).mount("#app");
</script>
</body>

结果

1
2
3
4
computer-methods.html:40 调用了计算属性result的getter
computer-methods.html:34 调用了getScore方法
computer-methods.html:34 调用了getScore方法
computer-methods.html:34 调用了getScore方法

12.4. 计算属性的缓存

  • 这是什么原因呢?
    • 这是因为计算属性会基于它们的依赖关系进行缓存;
    • 在数据不发生变化时,计算属性是不需要重新计算的;
    • 但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算;

12.5. 计算属性的setter和getter

  • 计算属性在大多数情况下,只需要一个getter方法即可,会将计算属性直接写成一个函数
  • 但是,如果确实想设置计算属性的值呢?
    • 这个时候给计算属性设置一个setter的方法;
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
<body>
<div id="app"></div>
<template id="vue">

<h2>{{fullName="getter setter"}}</h2>
<h2>{{firstName}}</h2>
<h2>{{lastName}}</h2>

</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
firstName: '',
lastName: ''
}
},
computed: {
fullName:{
get:function(){
return this.firstName+" "+this.lastName;
},
set:function(value){
const name = value.split(" ");
this.firstName = name[0];
this.lastName = name[1];
}
}
}
}).mount("#app");
</script>
</body>

12.6. 源码如何对setter和getter处理呢?

  • Vue内部是如何对传入的是一个getter,还是说是一个包含setter和getter的对象进行处理的呢?
  • 事实上非常的简单,Vue源码内部只是做了一个逻辑判断而已;

/packages/runtime-core/src/componentOptions.ts

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
if (computedOptions) {
for (const key in computedOptions) {
const opt = (computedOptions as ComputedOptions)[key]
// 判断是否是一个函数,函数取get,否则在opt中取get
const get = isFunction(opt)
? opt.bind(publicThis, publicThis)
: isFunction(opt.get)
? opt.get.bind(publicThis, publicThis)
: NOOP
if (__DEV__ && get === NOOP) {
warn(`Computed property "${key}" has no getter.`)
}
// 在opt中取set
const set =
!isFunction(opt) && isFunction(opt.set)
? opt.set.bind(publicThis)
: __DEV__
? () => {
warn(
`Write operation failed: computed property "${key}" is readonly.`
)
}
: NOOP
const c = computed({
get,
set
})
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => c.value,
set: v => (c.value = v)
})
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.COMPUTED, key)
}
}
}

13. watch

13.1. 认识侦听器watch

  • 在data返回的对象中定义了数据,这个数据通过插值语法等方式绑定到template中;

  • 当数据变化时,template会自动进行更新来显示最新的数据;

  • 但是在某些情况下,希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器watch来完成了;

  • 侦听器的用法如下:

  • 选项:watch

  • 类型:{ [key: string]: string | Function | Object | Array}

13.2. 侦听器案例

  • 举个例子:
    • 每当用户输入了最新的内容,获取到最新的内容;
    • 那么,就需要实时的去获取最新的数据变化;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<div id="app"></div>
<template id = "vue">
<input type="text" v-model="question">
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
question: ""
}
},
watch: {
question(newValue,oldValue){
console.log(newValue,oldValue);
}
}
}).mount("#app");
</script>
</body>

13.3. 侦听器watch的配置选项

  • 例子:

    • 当点击按钮的时候会修改info.name的值;
    • 这个时候使用watch来侦听info,可以侦听到吗?答案是不可以。
  • 这是因为默认情况下,watch只是在侦听info的引用变化,对于内部属性的变化是不会做出响应的:

    • 这个时候可以使用一个选项deep进行更深层的侦听;
    • watch里面侦听的属性对应的也可以是一个Object;
  • 还有另外一个属性,是希望一开始的就会立即执行一次:

    • 这个时候使用immediate选项;
    • 这个时候无论后面数据是否有变化,侦听的函数都会有限执行一次;
  • 对象中的对象采用的是浅拷贝,指向同一个地址,info的newValue,oldValue中 hobbies是一样的值

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
<body>
<div id="app"></div>
<template id = "vue">
<input type="text" v-model="info.hobbies.name">
<h2>{{info}}</h2>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
info: {
name: 'H John',
age: 18,
hobbies: {
name: 'ball'
}
}
}
},
watch: {
info: {
// watch只是在侦听info的引用变化,对于内部属性的变化是不会做出响应的
handler: function(newValue,oldValue){
console.log(newValue,oldValue);
},
// 使用一个选项deep进行更深层的侦听
deep: true,
// 希望一开始的就会立即执行一次
immediate: true
},
'info.hobbies.name': function(newValue,oldValue){
console.log(newValue,oldValue);
},
}
}).mount("#app");
</script>
</body>

13.4. 侦听器watch的其他方式

13.4.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
<body>
<div id="app"></div>
<template id = "vue">
<input type="text" v-model="info">
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
template: '#vue',
data: function(){
return {
info: ""
}
},
watch: {
// 传入回调数组,它们会被逐一调用
info: [
// 字符串方法名
"handle1",
function handle2 (val, oldVal) {
console.log("handle2 trigger");
},
{
handler: function handle3(val,oldVal){
console.log("handle3 trigger");
}
}
]
},
methods: {
handle1: function(val,oldVal){
console.log(val,oldVal);
}
}
}).mount("#app");
</script>
</body>

13.4.2. $watch

  • 在created的生命周期)中,使用 this.$watchs 来侦听;
    • 第一个参数是要侦听的源;
    • 第二个参数是侦听的回调函数callback;
    • 第三个参数是额外的其他选项,比如deep、immediate;
1
2
3
4
5
6
7
8
created(){
this.$watch('message',(newValue,oldValue)=>{
console.log(newValue,oldValue);
},{
deep: true,
imediate: true
})
}

14. v-model

  • 表单提交是开发中非常常见的功能,也是和用户交互的重要手段:

  • 比如用户在登录、注册时需要提交账号密码;

  • 比如用户在检索、创建、更新信息时,需要提交一些数据;

  • 这些都要求在代码逻辑中获取到用户提交的数据,通常会使用v-model指令来完成:

    • v-model指令可以在表单 input、textarea以及select元素上创建双向数据绑定;
    • 它会根据控件类型自动选取正确的方法来更新元素;
    • 尽管有些神奇,但 v-model 本质上不过是语法糖,它负责监听用户的输入事件来更新数据,并在某种极端场景 下进行一些特殊处理;
1
2
3
4
<template>
<input type="text" v-model="message">
<h2>{{message}}</h2>
</template>

14.1. v-model的原理

  • 官方有说到,v-model的原理其实是背后有两个操作:
    • v-bind绑定value属性的值;
    • v-on绑定input事件监听到函数中,函数会获取最新的值赋值到绑定的属性中;
1
2
3
<input type="text" v-model="message">
<!-- 等价于 -->
<input type="text" :value="message" @input="message=$event.target.value">

14.2. v-model 源码

/packages/runtime-dom/src/directives/vModel.ts

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
export const vModelText: 
ModelDirective<HTMLInputElement | HTMLTextAreaElement> = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
const castToNumber =
number || (vnode.props && vnode.props.type === 'number')
addEventListener(el, lazy ? 'change' : 'input', e => {
if ((e.target as any).composing) return
let domValue: string | number = el.value
if (trim) {
domValue = domValue.trim()
}
if (castToNumber) {
domValue = toNumber(domValue)
}
el._assign(domValue)
})
},
beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
}
}

const getModelAssigner = (vnode: VNode): AssignerFn => {
const fn =
vnode.props!['onUpdate:modelValue'] ||
(__COMPAT__ && vnode.props!['onModelCompat:input'])
return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
}

14.3. v-model绑定textarea

1
2
3
<label for="intro">
<textarea id="intro" v-model="article"></textarea>
</label>

14.4. v-model绑定checkbox

  • 单个勾选框:

    • v-model即为布尔值

    • 此时input的value并不影响v-model的值

      1
      2
      3
      4
      <label for="agree">
      <input type="checkbox" name="agree" id="agree" v-model="isAgree">
      </label>
      <h2>{{isAgree}}</h2>
  • 多个复选框:

    • 当是多个复选框时,因为可以选中多个,所以对应的data中属性是一个数组

    • 当选中某一个时,就会将input的value添加到数组中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <!-- 多选框 -->
      <div>
      <label for="basketball">
      <input type="checkbox" id="basketball" value="basketball" v-model="hobbies">basketball
      </label>
      <label for="football">
      <input type="checkbox" id="football" value="football" v-model="hobbies">football
      </label>
      <label for="tennis">
      <input type="checkbox" id="tennis" value="tennis" v-model="hobbies">tennis
      </label>
      <h2>{{hobbies}}</h2>
      </div>

14.5. v-model绑定radio

用于选择其中一项,选择的是input的value值

1
2
3
4
5
6
7
8
9
<div>
<label for="male">
<input type="radio" value="male" id="male" v-model="gender">male
</label>
<label for="female">
<input type="radio" value="female" id="female" v-model="gender">female
</label>
<h2>{{gender}}</h2>
</div>

14.6. v-model绑定select

  • 和checkbox一样,select也分单选和多选两种情况

  • 单选:只能选中一个值

    • v-model绑定的是一个值

    • 当选中option中的一个时,会将它对应的value赋值到fruit中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <div>
      <select v-model="fruit">
      <option value="apple">apple</option>
      <option value="orange">orange</option>
      <option value="banana">banana</option>
      </select>
      <h2>{{fruit}}</h2>
      </div>
      <script>
      data: function(){
      return {
      fruit: 'apple'
      }
      }
      </script>
  • 多选:可以选中多个值

    • v-model绑定的是一个数组

    • 当选中多个值时,就会将选中的option对应的value添加到数组fruit中

      1
      2
      3
      4
      5
      6
      7
      8
      <div>
      <select v-model="fruit" multiple size="2">
      <option value="apple">apple</option>
      <option value="orange">orange</option>
      <option value="banana">banana</option>
      </select>
      <h2>{{fruit}}</h2>
      </div>

14.7. v-model的值绑定

  • 在真实开发中,数据是来自服务器的,先将值请求下来,绑定到data返回的对象中, 再通过v-bind来进行值的绑定,这个过程就是值绑定

14.8. v-model修饰符

14.8.1. lazy

  • 默认情况下,v-model在进行双向绑定时,绑定的是 input事件,那么会在每次内容输入后就将最新的值和绑定 的属性进行同步;
  • 如果在v-model后跟上lazy修饰符,那么会将绑定的事件切换为 change 事件,只有在提交时(比如回车) 才会触发;
1
<input type="text" v-model.lazy="message">

14.8.2. number

  • 先来看一下v-model绑定后的值是什么类型的:

    • message总是string类型,即使在设置type为number也是string类型;
  • 如果希望转换为数字类型,那么可以使用 .number 修饰符:

    1
    <input type="text" v-model.number="score">
  • 另外,在进行逻辑判断时,如果是一个string类型,在可以转化的情况下会进行隐式转换的:

    • 下面的score在进行判断的过程中会进行隐式转化的;

      1
      2
      3
      4
      5
      const score="100";
      if(score>90){
      console.log("优秀");
      }
      console.log(typeof score);

14.8.3. trim

如果要自动过滤用户输入的空白字符,可以给v-model添加 trim 修饰符:

1
<input type="text" v-model.trim="message">
本文结束  感谢您的阅读