【Vue】系列一 - 基本使用


一、介绍

Vue(读音 /vjuː/,类似于 view)是一套用于构建用户界面渐进式JavaScript框架。

渐进式:可以自底向上逐层的应用。比如,只需要一个轻量小巧的核心库就可以构建一个简单应用或内嵌到自己的应用中,也可以引入各式各样的Vue插件构建一个复杂应用。

1.1. 特点

  • 采用组件化模式,提高代码复用率,且让代码更好维护。
    • 比如,一个.vue文件就可以是一个组件,因为.vue文件中包含html、css、js等内容。
  • 声明式编码,让编码人员无需直接操作DOM,提高开发效率。
    • 命令式:以往直接操作DOM的开发方式是命令式编程,按照顺序依次给出后指令执行一段代码。
    • 声明式:只需要声明一个指令或者代码,框架会自动解析执行,比如v-if、v-for
  • 使用虚拟DOM和优秀的Diff算法,尽量复用DOM节点。
    • 比如,一个ul列表,如果是静态元素,虚拟DOM和真实DOM几乎无差别,甚至真实DOM比虚拟DOM的渲染速度更快,因为虚拟DOM有资源开销。但如果是动态元素,由于Diff算法的存在,会把即将渲染的数据和旧数据进行Diff操作以形成虚拟DOM,让真实DOM只渲染新增加的元素。

1.2. MVVM

Vue采用的是MVVM架构模式。

MVVM的维基百科:https://zh.wikipedia.org/zh-cn/MVVM

Vue中的MVVM模型

1.3. 生命周期

生命周期又名生命周期回调函数、生命周期函数、生命周期钩子。

生命周期函数中的this指向的是vm或组件实例对象。

二、安装

2.1. CDN

1
2
3
4
<!-- 开发环境版本,包含了有帮助的命令行警告 --> 
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

2.2. 本地引入

把文件下载到本地进行引用。

2.3. npm

通过webpack或者CLI进行安装。

三、Vue实例

3.1. 挂载

new Vue()创建的实例中有很多$开头的方法,这些方法其实是在Vue的原型对象中,主要是提供给开发者使用。因此可以直接使用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
<div id="root">
<h1>Hello {{name.toUpperCase()}}</h1>
</div>

<script >
Vue.config.productionTip = false

// 正常写法
new Vue({
el: '#root',
data: {
name: 'Vue',
url: 'https://idbeny.com'
}
})

// 手动挂载
const vm = new Vue({
data: {
name: 'Vue',
url: 'https://idbeny.com'
}
})
v.$mount('#root')
</script>

应用场景:在合适的时间(例如延时)把Vue实例挂载到指定容器中。

3.2. 数据

对象式:正常情况下,Vue属性data的写法如下

1
2
3
4
5
6
7
8
9
<script>
new Vue({
el: '#root',
data: {
name: 'Vue',
url: 'https://idbeny.com'
}
})
</script>

函数式:其实data还可以写成函数的形式,如下所示

1
2
3
4
5
6
7
8
9
10
11
<script>
new Vue({
el: '#root',
data() {
return {
name: "Vue",
url: "https://idbeny.com"
};
}
})
</script>

两种写法有什么区别呢?

其实真实开发中,函数式写法使用频率最高。因为组件式开发时data必须使用函数式。

data使用函数式时一定不要使用箭头函数,应使用普通函数,因为涉及到data函数内部的this指向问题。

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
<!-- 箭头函数 -->
<script>
new Vue({
el: '#root',
data: () => {
console.log(this); // this指向的是Window实例对象
return {
name: "Vue",
url: "https://idbeny.com"
};
}
})
</script>

<!-- 普通函数 -->
<script>
new Vue({
el: '#root',
data() {
console.log(this); // this指向的是Vue实例对象
return {
name: "Vue",
url: "https://idbeny.com"
};
}
})
</script>

data中的所有数据Vue会自动做数据代理操作,这也是实现双向绑定的最核心做法。

3.3. 数据代理

Vue中的数据代理是通过vm对象(Vue实例对象)来代理data对象中属性的操作(读/写)。

使用数据代理的好处就是更加方便的操作data中的数据。

基本原理:

  • 通过Object.defineProperty()把data对象中所有属性添加到vm上。
  • 为每一个添加到vm上的属性,都指定一个getter/setter。
  • 在getter/setter内部去操作(读/写)data中对应的属性。

四、指令和语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="app">
<h2>Hello {{name}}</h2>
<h2>{{firstName}} {{lastName}}</h2>
<h2>{{counter * 2}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
name: 'daben',
firstName: 'ben',
lastName: 'yang',
counter: 10
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
})
</script>

4.1. 内容显示

  • 插值语法(双大括号)把数据插入到html中。双大括号内可以写js表达式。

    1
    2
    3
    4
    5
    6
    <h2>Hello {{name}}</h2>

    <!-- JS表达式 -->
    <h2>Hello {{a + b}}</h2>
    <h2>Hello {{name.toUpperCase()}}</h2>
    <h2>Hello {{a > b : '正确' : '错误'}}</h2>
  • v-once:该指令后面不需要跟任何表达式,表示元素和组件只渲染一次,不会随着数据的改变而改变。

    1
    <h2 v-once>{{message}}</h2>
  • v-html:向指定节点中渲染包含html结构的内容。

    • 和插值语法的区别:v-html会替换掉节点中所有的内容,并且可以识别html结构。
    • v-html有安全性问题:在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击。一定要在可信的内容上使用v-html,永远不要用在用户提交的内容上。
    1
    <h2 v-html="link"></h2>
  • v-text:和插值语法类似,都是用来显示文本(如果内容包含标签,也不会自动解析)。和插值语法不一样的是,它会整体替换标签中的内容。

    1
    <h2 v-text="message"></h2>
  • v-pre:用于跳过这个元素和它子元素的编译过程,Vue不再解析指令所在节点。

    1
    2
    3
    4
    <!-- 页面显示:Hello {{name}} -->
    <div v-pre>
    <h2>Hello {{name}}</h2>
    </div>
  • v-cloak:编译前不进行渲染(一般用于加载动画,在数据加载前不渲染元素)。

    1
    <h2 v-cloak>content</h2>

4.2. 数据绑定

4.2.1. 单向绑定

v-bind(语法糖:):用于绑定一个或多个属性值(单向绑定),或者向另一个组件传递props值。

1
2
3
4
5
<!-- 完整写法 -->
<img v-bind:src="avatarUrl" alt="">

<!-- 语法糖 -->
<img :src="avatarUrl" alt="">
  • 绑定class

    • 比如:当数据为某个状态时,字体显示红色;当数据另一个状态时,字体显示黑色。

    • 绑定class有两种方式:对象语法和数组语法。

    • 对象语法:class后面跟的是一个对象、字符串、数组。

      • 字符串写法适用于:类名不确定,要动态获取(data中取值)
      • 对象写法适用于:要绑定多个样式,个数不确定,名字不确定
      • 数组写法适用于:要绑定多个样式,个数确定,名字确定,但不确定用不用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      <!-- 用法一:从data中读取normal属性 -->
      <h2 class="basic" :class="normal">Hello World</h2>

      <!-- 用法二:直接通过{}绑定一个类 -->
      <h2 :class="{'active': isActive}">Hello World</h2>

      <!-- 用法三:也可以通过判断,传入多个值 -->
      <h2 :class="{'active': isActive, 'line': isLine}">Hello World</h2>

      <!-- 用法四:和普通的类同时存在,并不冲突 -->
      <!-- 注:如果isActive和isLine都为true,那么会有title/active/line三个类 -->
      <h2 class="title" :class="{'active': isActive, 'line': isLine}">Hello World</h2>

      <!-- 用法五:如果过于复杂,可以放在一个methods或者computed中 -->
      <!-- 注:classes是一个计算属性 -->
      <h2 class="title" :class="classes">Hello World</h2>
    • 数组语法:class后面跟的是一个字符串、数组、对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      <!-- 用法一:从data中读取arr属性(数组)  适用于:要绑定的样式个数不确定,名字也不确定 -->
      <h2 class="basic" :class="arr">Hello World</h2>

      <!-- 用法一:直接通过{}绑定一个类 -->
      <h2 :class="['active']">Hello World</h2>

      <!-- 用法二:也可以传入多个值 -->
      <h2 :class="['active', 'line']">Hello World</h2>

      <!-- 用法三:和普通的类同时存在,并不冲突 -->
      <!-- 注:会有title/active/line三个类 -->
      <h2 class="title" :class="['active', 'line']">Hello World</h2>

      <!-- 用法四:如果过于复杂,可以放在一个methods或者computed中 -->
      <!-- 注:classes是一个计算属性 -->
      <h2 class="title" :class="classes">Hello World</h2>
  • 绑定style

    • 用来绑定一些CSS内联样式,在写CSS属性名的时候,比如font-size,我们可以使用驼峰式 (camelCase)fontSize或短横线分隔 (kebab-case,记得用单引号括起来) 'font-size'

    • 绑定style有两种方式:对象语法和数组语法。

    • 对象语法:style后面跟的是一个对象类型(对象的key是CSS属性名称,对象的value是具体赋的值,值可以来自于data中的属性)

      1
      <h2 :style="{color: currentColor, fontSize: fontSize + 'px'}">Hello World</h2>
    • 数组语法:style后面跟的是一个数组类型,多个值以英文,分割即可

      1
      <div :style="[baseStyles, overridingStyles]"></div>

4.2.2. 双向绑定

v-model:元素和数据双向绑定v-model完整写法是v-model:value,因为v-model:value默认收集的是value值,所以可以简写为v-model

1
2
<input type="text" v-model="message">
<h2>{{message}}</h2>

当我们在输入框输入内容时,因为input中的v-model绑定了message,所以会实时将输入的内容传递给messagemessage发生改变)。

message发生改变时,因为上面我们使用Mustache语法,将message的值插入到DOM中,所以DOM会发生响应的改变。

所以,通过v-model实现了双向的绑定。

注意:v-model只能应用在表单类元素(如inputselect等)。因为v-model主要是影响value和data数据源。例如<h2 v-model:x="123"></h2>就不能使用v-model,因为没有输入源影响数据源。

  • 原理

    v-model其实是一个语法糖,它本质上是包含两个操作(原理):

    1. v-bind绑定一个value属性

    2. v-on指令给当前元素绑定input事件

    1
    2
    3
    <input type="text" v-model="message">
    // 等同于
    <input type="text" v-bind:value="message" v-on:input="message = $event.target.value">
  • 修饰符

    • lazy:默认情况下,v-model默认是在input事件中同步输入框的数据的。也就是说,一旦有数据发生改变对应的data中的数据就会自动发生改变。lazy修饰符可以让数据在失去焦点或者回车时才会更新

    • number:默认情况下,在输入框中无论我们输入的是字母还是数字,都会被当做字符串类型进行处理。但是如果我们希望处理的是数字类型,那么最好直接将内容当做数字处理。number修饰符可以让在输入框中输入的内容自动转成数字类型

    • trim:如果输入的内容首尾有很多空格,通常我们希望将其去除。trim修饰符可以过滤内容左右两边的空格

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <div>
    <input type="text" v-model.lazy="message">
    <p>当前内容(lazy):{{message}}</p>

    <input type="number" v-model.number="age">
    <p>当前年龄(number):{{age}} 类型:{{typeof age}}</p>

    <input type="number" v-model.trim="message">
    <p>当前内容(trim):{{message}}</p>
    </div>

4.3. 事件处理

v-on(语法糖@):事件监听。

  • 事件的回调需要配置在methods对象中,最终会在vm上;
  • methods中配置的函数,不要用箭头函数,否则this就不是vm;
  • methods中配置的函数,都是被Vue所管理的函数,this的指向是vm或组件实例对象;
  • @click="demo"@click="demo($event)"效果一致,但后者可以传参。

函数写在data中也是可以进行调用的,只不过函数没必要做数据代理,所以函数尽量放在methods中。

1
2
3
4
5
6
<!-- v-on不仅可以直接对属性进行操作,还可以传入一个methods中定义的函数 -->
<button v-on:click="counter++">按钮1</button>
<button v-on:click="btnClick">按钮2</button>

<!-- 语法糖 -->
<button @click="btnClick">按钮2</button>

4.3.1. 参数

  • 如果methods中定义的方法不需要额外参数,那么方法后的()可以不添加。
  • 如果方法本身中有一个参数,那么会默认将原生事件event作为参数传递进去。
  • 如果需要同时传入某个参数,同时需要event时,可以通过$event传入事件。
1
2
3
4
5
6
7
<button @click="btnClick(10, $event)">按钮</button>

methods: {
btnClick(count, event) {
console.log(event)
}
}

4.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
44
45
46
47
<!-- 阻止默认行为 -->
<button @click.prevent="doThis"></button>

<!-- 停止冒泡 -->
<button @click.stop="doThis"></button>

<!-- 点击回调只会触发一次 -->
<button @click.once="doThis"></button>

<!-- 使用事件的捕获模式(和阻止冒泡是相对应的) -->
<button @click.capture="doThis"></button>

<!-- 只有event.target是当前操作的元素时才触发事件 -->
<button @click.self="doThis"></button>

<!-- 触发滚动条(鼠标滚轮、键盘上下键等) -->
<ul @scroll="scrollTo"></ul>

<!-- 触发鼠标滚轮(事件函数调用完毕后才触发页面滚动) -->
<ul @wheel="scrollTo"></ul>

<!-- 事件的默认行为立即执行(优先执行滚动),无需等待事件回调执行完毕 -->
<ul @wheel.passive="scrollTo"></ul>

<!-- 阻止默认提交行为,没有表达式 -->
<button @submit.prevent></button>

<!-- 串联修饰符(有先后顺序,如下方:先停止冒泡再阻止默认事件) -->
<button @click.stop.prevent="doThis"></button>

<!-- 键修饰符,键别名 -->
<!--
Vue提供了以下的别名:
enter
delete
esc
space
tab(需要使用keydown,如果使用keyup就会被系统事件拦截)
up
down
left
right
-->
<button @keyup.enter="onEnter"></button>

<!-- 键修饰符,键代码(已废弃) -->
<button @keyup.13="onEnter"></button>

如果使用Vue未提供别名的按键,可以使用按键原始的key值去绑定,但注意要转为短横线命名(比如,CapsLock(大小写)使用的时候需要转换为@keyup.caps-lock=””)

系统修饰键(ctrl、alt、shift、meta(Windows的win键或Mac的command键))用法比较特殊:

  1. 配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
    • 比如需要触发ctrl键,需要先按下ctrl不松,再按下其他键才能触发。
    • 如果需要指定组合键,可以连写,比如ctrl和y键触发:@keyup.ctrl.y="onCtrl"
  2. 配合keydown使用:正常触发事件。

也可以使用keyCode去指定具体的按键(比如:@keyup.13="onEnter"),但是不推荐这样做。

定制按键别名:Vue.config.keyCodes.自定义键名 = 键码

4.4. 条件渲染

v-if、v-else-if、v-else、v-show

  • v-if:后面的条件为false时,对应的元素以及其子元素不会渲染。也就是根本没有不会有对应的标签出现在DOM中。

  • v-show:用法和v-if非常相似,也用于决定一个元素是否渲染。

    v-ifv-show都可以决定一个元素是否渲染,那么开发中我们如何选择呢?

    两者的区别:

    1. v-if当条件为false时,压根不会有对应的元素在DOM中。

    2. v-show当条件为false时,仅仅是将元素的display属性设置为none而已(占位)。

    两者的选择:

    1. 当需要在显示与隐藏之间切片很频繁时,使用v-show,避免资源开销

    2. 当只有一次切换时,通过使用v-if

v-iftemplate进行组合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 原始代码 -->
<h2 v-if="n === 1">1</h2>
<h2 v-if="n === 1">2</h2>
<h2 v-if="n === 1">3</h2>

<!-- 改进版本1 -->
<div v-if="n === 1">
<h2>1</h2>
<h2>2</h2>
<h2>3</h2>
</div>

<!-- 改进版本2(最终DOM上是没有template的,因此不会破坏原始代码结构) -->
<template v-if="n === 1">
<h2>1</h2>
<h2>2</h2>
<h2>3</h2>
</template>

注意:template不能和v-show进行组合。

4.5. 列表渲染

v-for

4.5.1. 数组

1
2
3
4
5
6
<ul>
<!-- 不使用索引值 -->
<li v-for="item in movies">{{item.name}}</li>
<!-- 使用索引值 -->
<li v-for="(item, index) in movies">{{index + 1}}.{{item.name}}</li>
</ul>

检测数组更新:因为Vue是响应式的,所以当数据发生变化时,Vue会自动检测数据变化,视图会发生对应的更新。Vue中包含了一组观察数组编译的方法,使用它们改变数组也会触发视图的更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
methods: {
updateData() {
// push
this.names.push('daben', 'jack')
// pop
this.names.pop()
// unshift
this.names.unshift('daben', 'jack')
// shift
this.names.shift()
// splice
this.names.splice(2)
// sort
this.names.sort()
// reverse
this.names.reverse()
// 不会修改
// this.names[1] = 'rose'
// 这样才会修改
this.names.splice(1, 1, 'rose')
Vue.set(this.names, 1, 'rose')
}
}

只要不会触发原数组变化的API都不会触发响应式,比如filter,[index],slice

4.5.2. 对象

1
2
3
<ul>
<li v-for="(value, key, index) in info">{{value}}-{{key}}-{{index}}</li>
</ul>

如果需要动态给对象添加/删除属性,需要使用特殊的API才能触发响应式:

1
2
3
4
5
// 使用组件实例修改
this.$set(this.person, 'sex', '男')

// 使用Vue修改
Vue.$set(this.person, 'sex', '男')

删除属性不要使用JS的API(delete this.person.name),因为不会触发响应式。

1
2
3
4
// 删除对象指定属性
this.$delete(this.person, 'name')

Vue.$delete(this.person, 'name')

4.5.3. key

官方推荐我们在使用v-for时,给对应的元素或组件添加上一个:key属性。主要用来Diff算法时,让程序不要重用元素。key的作用主要是为了高效的更新虚拟DOM

index作为key

id作为key

面试题:Vue/React中的key有什么作用(key的内部原理)?

  1. 虚拟DOM中key的作用:key是虚拟DOM对象的标识,当状态中的数据发生变化时,Vue会根据【新数据】生成【新虚拟DOM】,随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:
    • 旧虚拟DOM中找到了与新虚拟DOM相同的key:
      • 若虚拟DOM中内容没变,直接使用之前的真实DOM(复用)
      • 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
    • 旧虚拟DOM中未找到与新虚拟DOM相同的key:
      • 创建新的真实DOM,随后渲染到页面
  2. 用index作为key可能会引发的问题:
    • 若对数据进行逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实DOM更新(界面效果没问题,但效率低)
    • 如果结构中还包含输入类的DOM,会产生错误DOM更新(界面有问题)
  3. 开发中如何选择key?
    • 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值
    • 如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于展示,使用index作为key是没有问题的

4.6. 计算属性

computed:要用的属性不存在,需通过已有属性计算得出。

原理:底层借助了Object.defineproperty方法提供的getter和setter。

get函数什么时候执行?

  • 初次读取时会执行一次
  • 当依赖的数据发生改变时会被再次调用

计算属性最终会出现在vm上,直接读取使用即可。如果计算属性要被修改,那必须写set函数去响应修改,且set函数中要引起计算时依赖的数据发生变化。

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 id="app">
<h2>{{firstName}} {{lastName}}</h2>
<h2>{{fullName}}</h2>
</div>

<script>
let vm = new Vue({
el: '#app',
data: {
firstName: 'ben',
lastName: 'yang'
},
/* getter(简写,不考虑写)
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
*/
// setter和getter(完整写法)
computed: {
fullName: {
get() { // 调用条件: 1. 初次读取fullName时 2. 依赖的数据发生变化时
return this.firstName + ' ' + this.lastName
},
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[1]
}
}
}
})
</script>

methodscomputed都可以实现类似的功能,那么为什么还要计算属性呢?

原因:计算属性会进行缓存(复用),如果多次使用时,计算属性只会调用一次。效率更高,节省资源。

4.7. 监视

watch可以检测属性和计算属性的值变化。

  1. 当被监视的属性变化时,回调函数handler自动调用
  2. 监视的属性必须存在才能进行监视
  3. 监视有两种写法:
    1. new Vue时传入watch配置
    2. 通过vm.$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
<script>  
let vm = new Vue({
el: "#root",
data: {
isHot: false
},
computed: {
info() {
return this.isHot ? '炎热' : '凉爽'
}
},
methods: {
changeWeather() {
this.isHot = !this.isHot
}
},
watch: {
isHot: {
immediate: true,
handler(newValue, oldValue) {
console.log(`isHot由${newValue}改变为${oldValue}`);
}
}
},
});
</script>

immediate:是否从程序运行时就开始监控,默认是false

  • 如果immediate: true,如上程序首次运行时输出:isHot由false改变为undefined
  • 如果immediate: false,只有监听的属性值改变时才会监听到

deep:是否开启深度监视,监视多级结构中所有属性的变化。默认false

使用Vue实例上的$watch也可以监听属性值的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script> 
let vm = new Vue({
el: "#root",
data: {
isHot: false
}
});
vm.$watch('isHot', {
immediate: true,
handler(newValue, oldValue) {
console.log(`isHot由${newValue}改变为${oldValue}`)
}
});
</script>

如果要监听属性中多级结构对象里面的某个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
<script>  
let vm = new Vue({
el: "#root",
data: {
info: {
name: "",
age: 0
}
},
watch: {
// 监视多级结构中某个属性的变化
'info.name': {
immediate: true,
handler(newValue, oldValue) {
console.log(`由${newValue}改变为${oldValue}`);
}
},
// 监听对象中所有值的变化
info: {
deep: true,
immediate: true,
handler(newValue, oldValue) {
console.log(`由${newValue}改变为${oldValue}`);
}
}
},
});
</script>

注意:Vue默认情况下是可以监视到多层级结构属性值变化的,但watch默认不能检测对象内部值的变化(只能检测一层)。给watch配置deep:true才可以检测对象内部值的改变(可以检测多层)。

当被监视的属性,只需要handler时,可以简写为:

1
2
3
4
5
6
7
8
9
watch: {
isHot(newValue, oldValue) {
console.log(`isHot由${newValue}改变为${oldValue}`);
}
}

vm.$watch("abc", function (newValue, oldValue) {
console.log(`isHot由${newValue}改变为${oldValue}`);
});

watchcomputed的区别?

  1. computed能完成的功能,watch都可以完成
  2. watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作。

使用过程中,只要遵循以下两个小原则就可以:

  1. 所有被Vue管理的函数,最好写成普通函数,这样this的指向才是vm或组件实例对象。
  2. 所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等),最好写成箭头函数,这样this的指向才是vm或组件实例对象。

原理

  1. Vue会监视data中所有层次的数据。
  2. 如何检测对象中的数据?通过setter实现监视,且要在new Vue时就传入要监测的数据。
    • 对象中后追加的属性,Vue默认不做响应式处理。如果需要给后添加的属性做响应式,请使用如下API:Vue.set(target, propertyName/index, value)vm.$set(target, propertyName/index, value)
  3. 如何监测数组中的数据?通过包裹数组更新元素的方法实现,本质就是做了两件事:
    • 调用原生对应的方法对数组进行更新
    • 重新解析模板,进而更新页面
  4. 在Vue修改数组中的某个元素一定要用如下方法:
    • push(),pop(),shift(),unshift(),splice(),sort(),reverse()(都会改变原数组)
    • Vue.set()vm.$set()

特别注意:Vue.set()vm.$set()不能给vm或vm的根数据对象添加属性。

4.7. 过滤器

定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。

语法:

  • 注册过滤器
    • 全局:Vue.filter(name, callback)
    • 局部:new Vue(filters:{})
  • 使用过滤器
    • {{ xxx | 过滤器名 }}(经常使用)
    • v-bind:属性 = "xxx | 过滤器名"(很少使用)

过滤器也可以接收额外参数、多个过滤器也可以串联。过滤器并没有改变原来的数据,是产生新的数据。

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
<!-- 默认传参:前面的参数会作为形参传给后面的过滤器 -->
<h2>现在时间:{{time | timeFormatter}}</h2>

<!-- 多个过滤器:从左到右依次传参,后面过滤器的参数是前面过滤器的返回值 -->
<h2>现在时间:{{time | timeFormatter | timeSlice}}</h2>

<!-- 多个参数:过滤器默认会传递前面的参数(作为第一个形参),所以只需要传递需要的参数即可 -->
<h2>现在时间:{{time | timeFormatter('YYYY年MM日DD日')}}</h2>

<script>
// 全局过滤器(必须写在实例创建前)
Vue.filter('timeSlice', function(value) {
return value.slice(0, 4)
})

new Vue({
el: "#root",
data: {
time: 1656649136275
},
// 局部过滤器
filters: {
timeFormatter(value, fmtStr="YYYY-MM-DD HH:mm:ss") {
return dayjs(value).format(fmtStr)
}
}
});
</script>

五、自定义指令

使用Vue提供的directives可以自定义指令。

指令函数调用时机:

  • 与元素绑定成功时调用时(第一次,此时并没有渲染到页面)
  • 指令所在模板被重新解析时
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="root">
<h2>原始数据:<span v-text="n"></span></h2>
<h2>放大数据:<span v-big="n"></span></h2>
<button @click="n++">n++</button>
</div>

<script>
new Vue({
el: "#root",
data: {
n: 1,
},
directives: {
// element:DOM元素
// binding:元素属性绑定信息(指令名、指令对应的表达式、值等)
big(element, binding) {
element.innerText = binding.value * 10;
}
}
});
</script>
</body>

上面的代码执行没有问题。但是下面的代码就不能使input聚焦,因为在执行element.focus()时,input还没有被Vue渲染到DOM上(只有渲染到DOM上,input才能执行聚焦方法)。此时就需要编写成键值对形式了。

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
<body>
<div id="root">
<!-- 需求:n值变化时,v-big指令让n值放大10倍 -->
<h2>原始数据:<span v-text="n"></span></h2>
<h2>放大数据:<span v-big="n"></span></h2>
<button @click="n++">n++</button>

<!-- 需求:n值变化时,v-fbind指令让input自动聚焦(出现光标) -->
<input type="text" v-fbind:value="n">
</div>

<script>
Vue.config.productionTip = false;
let vm = new Vue({
el: "#root",
data: {
n: 1,
},
directives: {
big(element, binding) {
element.innerText = binding.value * 10;
},
fbind(element, binding) {
element.value = binding.value
element.focus()
}
}
});
</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
<script>
let vm = new Vue({
el: "#root",
data: {
n: 1,
},
directives: {
fbind: {
// 指令与元素成功绑定时(一上来)
bind(element, binding) {
element.value = binding.value
},
// 指令所在元素被插入页面后
inserted(element, binding) {
element.focus()
},
// 指令所在模板被重新解析时
update(element, binding) {
element.value = binding.value
}
}
}
});
</script>

其实fbind(){}就是bindupdate的合体写法。

和watch类似,上面的写法是局部自定义指令,自定义全局指令写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
Vue.$directive("big", function(element, binding) {
element.innerText = binding.value * 10;
});

Vue.$directive("fbind", {
// 指令与元素成功绑定时(一上来)
bind(element, binding) {
element.value = binding.value;
},
// 指令所在元素被插入页面后
inserted(element, binding) {
element.focus();
},
// 指令所在模板被重新解析时
update(element, binding) {
element.value = binding.value;
},
});
</script>

命名注意:指令定义时不加v-,但使用时必须要加v-。指令名如果是多个单词,要使用kebab-case命名方式,不要用camelCase命名。