Vue 2 学习笔记

简介

Vue 是一套用于构建用户界面的前端框架。

Vue 框架的特性,主要体现在如下两方面:

  • 数据驱动视图:在使用了 Vue 的页面中,Vue 会监听数据源的变化,当发生变化时,自动将新的数据重新渲染到页面上,数据驱动视图是单向的数据绑定,即根据数据(源)来驱动视图(页面)。
  • 双向数据绑定:在监听数据源的前提下同时还监听页面结构(主要指的是表单控件),当表单值变化时,自动将最新的表单内容同步到数据源。不再需要再手动为表单元素添加事件然后操作 DOM 元素来获取表单元素最新的值。

Image

MVVM 是 Vue 实现数据驱动视图和双向数据绑定的核心原理。MVVM 指的是 Model、View 和 ViewModel,每个动态的 HTML 页面都可以抽象成这三个部分。

image-20211117173928391

MVVM 模式并不是 Vue 所特有的,基本所有现代的前端框架例如 React 和 Angular 都采纳了 MVVM 思想。 MVVM 其实和后端的 MVC 理念上很相近,都是将数据和视图分开,用专门的对象来管理同步它们之间的联系。

就 Vue 具体而言

  • Model 表示当前页面渲染时所依赖的数据源,通常是一个 JavaScript 对象或者是 JSON 字符串。
  • View 表示当前 HTML 页面所渲染的 DOM 结构。
  • ViewModel 是 MVVM 的核心,它表示一个具体的 Vue 的实例对象。它将页面的数据源(Model)和页面的结构(View)连接在了一起。当 Model 数据源发生变化时,会被 ViewModel 监听到,ViewModel 会根据最新的数据源自动更新页面的 View 结构。当页面中表单元素的值发生变化时,也会被 ViewModel 监听到,ViewModel 会把变化过后最新的值自动同步到 Model 数据源中。

image-20211117172651667

Vue 共有 3 个大版本:

  • 1.x 版本的 Vue 几乎被淘汰,不再建议学习与使用。
  • 2.x 版本的 Vue 是目前企业级项目开发中的主流版本,但是近几年会逐步淘汰。
  • 3.x 版本的 Vue 于 2020 年 9 月发布,尚未在企业级项目开发中普及和推广,生态在逐渐完善,是未来企业级项目开发的趋势。

下面的内容只涉及 Vue 2 。

推荐在浏览器上安装 Vue 官方提供的 Vue Devtools 调试拓展工具,能够更方便调试和开发 Vue 应用。在浏览器中访问一个使用了 vue 的页面,打开浏览器的开发者工具,切换到 Vue 面板,即可使用 vue-devtools 拓展调试当前的页面。

使用

  1. 在 HTML 文件中导入 vue.js 脚本文件。
  2. 在页面中声明一个将要被 Vue 所控制的 DOM 区域。
  3. 通过构造函数创建 Vue 对象,该对象即一个 ViewModel 实例,构造时传入对象的 el 属性指向的 DOM 区域即 View 视图, data 属性即 Model 数据源。

注意:如果 el 属性指定的选择器匹配了多个元素,Vue 对象只会控制匹配到第一个 DOM 元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<!-- 1. 导入 vue.js 的 脚本文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<!-- 2. 在页面中声明一个将要被 Vue 所控制的 DOM 区域 -->
<div id="app">
{{ message }}
</div>

<!-- 3. 创建 Vue 实例 -->
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello,vue'
}
})
</script>
</body>

指令

指令(Directives)是 vue 为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构。

指令 (Directives) 是带有 v- 前缀的特殊 attribute。

vue 中的指令按照不同的用途可以分为如下 6 大类:

  • 内容渲染指令
  • 属性绑定指令
  • 事件绑定指令
  • 双向绑定指令
  • 条件渲染指令
  • 列表渲染指令

内容渲染指令

  • v-text:将属性值渲染到元素标签,会完全覆盖元素内原来的内容。

    1
    <p v-text="gender">性别</p>
  • {{ }}:插值表达式中的属性值会被自动渲染替换。Mustache 双大括号语法严格来说并不能算作 Vue 指令,是 Vue 为开发者提供的模板语法。插值表达式外表达式中可以直接使用 Vue 实例上的属性和方法,this 可以省略。还支持 Javascript 表达式的运算,表达式支持有限的 JavaScript 语法,不能有太复杂的语句。

    1
    <p>性别:{{gender}}</p>
  • v-htmlv-text 指令和 {{}} 语法只能渲染纯文本内容,属性值将被解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,需要使用 v-html 把包含 HTML 标签的字符串渲染为页面的 HTML 元素。

属性绑定指令

v-bind:为元素的属性动态绑定属性值,由于 v-bind 指令在开发中使用频率非常高,因此,Vue 官方为其提供了简写形式(简写为英文的 : )。

1
2
3
4
5
<!-- 完整语法 -->
<a v-bind:href="url">...</a>

<!-- 缩写 -->
<a :href="url">...</a>

插值表达式只能用在元素的内容,不能用在元素的属性值上

事件绑定指令

v-on:为 DOM 元素绑定事件监听, v-on 指令在开发中使用频率非常高,因此,Vue 官方为其提供了简写形式(简写为英文的 @)。

1
2
3
4
5
<!-- 完整语法 -->
<a v-on:click="addCount">...</a>

<!-- 缩写 -->
<a @click="addCount">...</a>

DOM 对象有 onclickoninputonkeyup 等原生事件,替换为 vue 的事件绑定形式后,分别为:v-on:clickv-on:inputv-on:keyup,通过 v-on 绑定的事件处理函数,需要在 Vue 对象的 methods 节点中进行声明。

1
2
3
4
5
6
7
8
9
10
11
const vm = new Vue({
el: '#app',
data: {
count: 0,
},
methods: {
addCount() {
this.count += 1;
}
}
})

如果事件处理函数中的代码足够简单,可以直接以 JavaScript 表达式形式写到绑定处,不用再独立成函数,表达式中可以直接使用 Vue 实例上的属性和方法,this 必须要省略。

1
<a @click="count += 1">...</a>

如果 v-on 指令绑定的是函数名,当事件发生时会参默认传入事件对象 event,可以通过参数接收到事件对象 event

1
2
3
4
5
6
7
8
9
10
11
const vm = new Vue({
el: '#app',
data: {
count: 0,
},
methods: {
doSomething(e) {
console.log(e)
}
}
})

如果 v-on 指令绑定的是函数调用语句,则不会再隐式传入事件对象 event,可以在后面跟上小括号显式传递参数。这样在函数中第一个参数接收到的就不再是事件对象 event,而是显式传入的实参。

1
<a v-on:click="addCount(3)">...</a>

注意:v-on:click="addCount"v-on:click="addCount()" 不一样,前者会默认传入事件对象 event,而后者不会传入任何实参。

为了解决事件参数对象 event 被覆盖的问题,Vue 提供的特殊变量 $event,用来表示原生的事件参数对象 event,需要使用到事件对象时显式传入。

1
<a v-on:click="addCount(3, $event)">...</a>

接收时按序接收:

1
2
3
4
5
6
7
8
9
10
11
12
const vm = new Vue({
el: '#app',
data: {
count: 0,
},
methods: {
doSomething(step, e) {
console.log(step)
console.log(e)
}
}
})

在事件处理函数中调用 event.preventDefault()event.stopPropagation() 是非常常见的需求。因此,Vue 提供了事件修饰符的更方便的对事件的触发进行控制。常用的 5 个事件修饰符如下:

image-20211125142001400

1
<a v-on:click.prevent="addCount">...</a>

在监听键盘事件时,经常需要通过事件对象的 keyCode 属性判断具体的按键,Vue 提供了按键修饰符可以为快速为特定按键添加事件。

1
<input @keyup.esc="clearInput">

双向绑定指令

v-model:双向数据绑定指令,可以在不手动操作 DOM 的前提下,快速获取表单的数据。

1
<input type="text" v-model="username"/>

v-model 指令会自动判断所绑定的 <input> 表单元素的 type 属性值,如果 type 的值为 "radio" 或者 "checkbox" 等选择框则会把数据值绑定到 checked 属性上,如果值是 "text" 等文本框则绑定到 value 属性上。使用了 v-mode 再指定 value 或者 checked 属性值没有意义。

为了方便对用户输入的内容进行处理,Vue 为 v-model 指令提供了 3 个修饰符,分别是:

image-20211125164828560

条件渲染指令

  • v-if:可以动态地创建或移除 DOM 元素,达到控制元素显示与隐藏效果。v-if 指令可以配合 v-elsev-else-if 指令一起使用。

    1
    2
    3
    4
    <div v-if="type==='A'"></div>
    <div v-else-if="type==='B'"></div>
    <div v-else-if="type==='C'">及格</div>
    <div v-else></div>
  • v-show:动态为元素添加或移除 style="display: none;" 属性,从而控制元素的显示与隐藏。

  • 最佳实践:v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。如果需要非常频繁地切换,则使用 v-show 较好,如果在运行时条件很少改变,则使用 v-if 较好。在实际开发中,绝大多数情况,不用考虑性能问题,直接使用 v-if 就好了。

列表渲染指令

v-for:列表渲染指令,基于一个数组来循环渲染一个列表结构。

假设在 Vue 实例的对象已经准备好了下面的数据:

1
2
3
4
5
6
data: {
userList: [
{id: 1, name: 'zs'},
{id: 2, name: 'ls'}
]
}

v-for 指令需要使用 item in items 形式的语法,其中 items 是待循环的数组 item 是被循环的每一项。

1
2
3
<ul>
<li v-for="user in userList">姓名是:{{user.name}}</li>
</ul>

v-for 指令还支持一个可选的第二个参数,即当前项的索引。语法格式为 (item, index) in items

1
2
3
<ul>
<li v-for="(user, i) in userList">姓名是:{{user.name}},索引是:{{i}}</li>
</ul>

当列表的数据变化时,默认情况下,Vue 会尽可能的复用已存在的DOM 元素,从而提升渲染的性能。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新。为了让 Vue 跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲染的性能,需要为每个列表项提供一个唯一的 key 属性。

建议使用 v-for 指令时一定要指定 key 的值,既提升性能、又防止列表状态紊乱。低版本 Vue 的 v-for 指令不加 :key 属性可能报错。

注意:key 的值只能是字符串或数字类型,且值必须具有唯一性不能重复。通常建议把数据项的 id 属性的值作为 key 的值,因为 id 属性的值往往具有唯一性,使用索引 index 的值当作 key 的值没有任何意义,尽管索引也具有唯一性,但是和列表项之间没有绑定关系,例如当列表重新排序,或者前面插入或删除了列表项的索引都可能发生变化。

1
2
3
<ul>
<li v-for="user in userList" :key="user.id">姓名是:{{user.name}}</li>
</ul>

建议的指令书写顺序,先渲染指令, 再属性绑定指定,最后事件绑定指令。

1
<Goods v-for="item in list" :key="item.id" :title="item.goods_name"  @click="stateChange"></Goods>

过滤器

过滤器(Filters)常用于文本的格式化。过滤器可以用在两个地方:双括号插值表达式和 v-bind 属性绑定。
过滤器应该被添加在 JavaScript 表达式的尾部,由“管道符”进行调用,示例代码如下:

1
2
3
4
5
<!-- 在双花括号中调用 capitalize 过滤器对 message 进行格式化 -->
<p>{{ message | capitalize }}</p>

<!-- 在 v-bind 指令中调用 formatId 过滤器对 rawId 进行格式化 -->
<div v-bind:id="rawId | formatId"></div>

在创建 Vue 实例期间,可以在 filters 属性节点中定义函数,该对象中的每一个函数都是都是一个过滤器,在过滤器函数中,一定要通过 return 返回值。

1
2
3
4
5
6
7
8
9
10
11
const vm = new Vue({
el: '#app',
data: {
message: 'hello,vue'
},
filters: {
capitalize: function (value) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
})

上面在 filters 节点下定义的过滤器被称为私有过滤器或者本地过滤器,只能在当前 Vue 实例 el 属性所控制的区域内使用。如果希望在多个 Vue 实例之间共享过滤器,则可以在创建 Vue 实例之前按照如下的格式定义全局过滤器:

1
2
3
4
5
// 第一个参数是全局过滤器的名字
// 第二个参数是全局过滤器的函数处理逻辑
Vue.filter('capitalize', function (value) {
return value.charAt(0).toUpperCase() + value.slice(1)
})

注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器。

过滤器的本质是 JavaScript 函数,可以在接收参数,在调用过滤器时传入参数。过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。

1
2
3
Vue.filter('filterA', (msg, arg1, arg2) => {
// 具体的代码逻辑
})

上面 filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数。

1
<p>{{ message | filterA('arg1', arg2) }}</p>

过滤器通过串联可以达到连续调用多个过滤器的效果,例如下面 message 的值先交给 filterA 处理,然后再将 filterA 的结果传递到 filterB 处理,最后将处理结果渲染到页面上。

1
<p>{{ message | filterA | filterB }}</p>

注意:过滤器仅在 Vue 2.x 和 1.x 中受支持,在 Vue 3.x 的版本中剔除了过滤器相关的功能,官方建议使用计算属性或方法代替被剔除的过滤器功能。

侦听器

侦听器允许开发者监视某一数据,只要数据变化发生就会触发侦听器,自动调用特定函数,从而针对数据的变化做特定的操作。

在创建 Vue 实例时通过 watch 节点中的函数来定义侦听器,函数名必须和要监听的数据名相同,要监视哪个数据的变化,就把该数据名作为函数名。该函数接收两个参数,第一个参数是变化后的新值,第二参数是变化之前的旧值。

1
2
3
4
5
6
7
8
9
10
11
const vm = new Vue({
el: '#app',
data: {
message: 'hello,vue'
},
watch: {
message: function (newVal, oldVal) {
console.log(newVal, oldVal)
}
}
})

除了上面通过方法格式定义侦听器外,还可以通过对象形式来定义监听器,这样能对侦听器做更多的配置。默认情况下,数据在页面初次渲染时变化不会触发绑定的侦听器。如果想让侦听器立即被调用,则需要设置对象的 immediate 选项。如果侦听的属性是一个复杂数据类型(对象),对象中的属性值发生了变化也不会触发绑定的侦听器。此时需要设置 deep 选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const vm = new Vue({
el: '#app',
data: {
user: {
username: 'admin',
password: 'pwd'
}
},
watch: {
user: {
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
immediate: true,
deep: true,
}
}
})

如果只想监听对象中单个属性的变化,则可以按照如下的方式定义侦听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const vm = new Vue({
el: '#app',
data: {
user: {
username: 'admin',
password: 'pwd'
}
},
watch: {
// 监听单个子属性,属性名一定要加引号
"user.username": function (newVal, oldVal) {
console.log(newVal, oldVal)
}
}
})

计算属性

计算属性指的是通过一系列运算之后,最终得到一个属性值。例如下面声明了一个计算属性 reversedMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})

虽然计算属性在声明的时候被定义为方法,但是计算属性可以看做是 Vue 实例对象的一个属性,就像定义在 data 数据节点中的属性一样。这个动态计算出来的属性值可以被内容渲染指令(包括插值表达式)直接使用,或在 methods 节点定义的方法通过通过 this 来使用。计算属性会缓存计算的结果,当计算属性被访问时立即返回之前的计算结果,只有计算属性依赖的数据变化时,才会重新进行运算。

VUL CLI

Vue CLI 是 Vue 官方提供的的 CLI(命令行工具),为单页面应用(SPA,Single Page Application)快速搭建繁杂的脚手架。它为现代前端工作流提供了开箱即用的构建设置,简化了程序员基于 webpack 创建工程化的 Vue 项目的过程,让开发者可以专注在撰写应用上,而不必花好几天去纠结 webpack 配置的问题。

在终端中使用以下的命令在全局安装 Vue CLI 工具。安装成功后可以使用 vue -V 查看版本信息。

1
npm i -g @vue/cli

在终端中使用下面命令既可以使用 Vue CLI 工具快速生成一个 Vue 项目。

1
vue create 项目的名称

注意:项目名称不能包含中文和空格。

使用 Vue CLI 工具生成的项目目录中有以下初始文件:

  • /assets 文件夹:存放项目中用到的静态资源文件,例如:CSS 样式表文件、图片资源。
  • /components 文件夹:封装的、可复用的 Vue 组件,都要放到 components 目录下。
  • /App.vue:项目的根组件,用来编写待渲染的模板结构。
  • /public/index.html:项目的首页,其中预留了 <div id="app"></div> 元素,vue-template-compiler 包会把 Vue 组件(模板文件)解析转化成 .js 文件注入到 index.html 中,浏览器执行脚本文件得到用组件渲染得到 DOM 替换这个元素。
  • /main.js :Webpack 项目的入口文件。整个项目的运行,要先执行 main.jsmain.jsApp.vue 根组件渲染到了 /public/index.html 所预留的区域中(通过 Vue 实例的 el 属性或者 $mount() 方法)。render 函数中渲染的是哪一个 Vue 组件,这个组件就是根组件。

组件

定义

Vue 是一个支持组件化开发的前端框架。后缀名是 .vue 文件本质上就是一个 Vue 组件。

组件化开发指的是:根据封装的思想,把页面上可重用的 UI 结构封装为组件,从而方便项目的开发和维护。

每个 Vue 组件都由 3 部分构成,分别是:

  • <template>:组件的模板,其中里面编写 HTML 结构,只能包含唯一的根节点。模板经过 Vue 渲染后替换掉原来的占位 DOM 元素。
  • <script>: 组件的行为,其中编写 JavaScript 代码,封装组件的业务逻辑。该组件相关的 data 数据、methods 方法需要定义到默认导出对象中。
    注意:data 必须是一个函数,通过函数 return 返回真正的数据对象,而不能直接使用对象声明。这样能保证每个组件的实例都拥有一份数据的拷贝,而不是共用一个数据对象。
  • <style>: 组件的样式,其中编写 CSS 样式,美化当前组件的 UI 结构。默认里面只能是 CSS 语法,如果想使用 Less 语法,需要添加属性对 lang="less"
    注意:默认情况下,组件中的样式会全局生效,影响整个页面,因此很容易造成多个组件之间的样式冲突问题。给组件的 style 标签添加 scoped 属性,可以防止组件之间的样式冲突问题。但是这样当前组件的样式对其子组件是不生效的。在使用第三方组件库的时候,如果想在父组件中改造子组件的默认样式,就需要使用 /deep/ 深度选择器。

注意:每个组件中必须包含 <template> 模板结构,而 <script><style> 标签是可选的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
用户名是:{{username}}
</div>
</template>

<script>
export default {
data() {
return {
username: "zs"
}
}
}
</script>

<style lang="less" scoped>
/deep/ div {
color: red;
}
</style>

使用

组件在被封装好之后,彼此之间是相互独立的,不存在相互联系。当组件之间相互使用嵌套时,根据彼此的嵌套关系,形成了父子关系、兄弟关系。

在一个组件中使用其他组件作为子组件的的步骤:

  1. 使用 ES6 import 语法导入需要的组件。
  2. 在组件的默认导出对象的 components 节点注册组件,自定义组件的注册名称一般大写开头。
  3. <template> 中以标签形式使用刚才注册的组件。

注意:组件自己不能嵌套使用自己,会形成无限递归导致栈溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<Left></Left>
</template>

<script>
// 1. 导入 Left 组件
import Left from '@/components/Left.vue'

export default {
// 2. 在 components 中注册,如果属性名和属性值相同可以简写
components: {
Left: Left
}
}
</script>

通过 components 节点注册的是私有子组件,只能在该组件中使用,其他没有注册过的组件中不能使用。在 Vue 项目的 main.js 入口文件中,通过 Vue.component() 方法,可以注册全局组件。全局组件注册一次,所有其他组件中都可以使用。

1
2
3
4
// 1. 导入要被注册的全局组件
import Count from '@/components/Count.vue'
// 2. 使用 Vue.component() 方法注册,第一个参数是注册后的使用名称,第二参数是被导入的组件
Vue.component('Count', Count)

自定义属性

通过组件的 props 节点可以获取到组件的自定义属性,这样使用组件的时候可以通过自定义属性来传递参数,提高了组件的复用性。

props 数组则每一个字符串元素都代表一个自定义属性。

1
2
3
4
5
<script>
export default {
props: ["start"]
};
</script>

假设一个 <Count> 组件向上面一样,在 props 节点中声明了 start 作为自定义属性,那么其他组件在使用这个组件时可以通过 <Count start="1"></Count> 来为组件指定初始值。

注意:直接通过标签属性传值实际接收到的是字符串,如果想传递的是数字类型数据,应该使用 v-bind 属性绑定指令来给属性赋值,因为 v-bind 后面的实际上是 JavaScript 表达式。例如 <Count start="1"></Count> 接收到的 start 实际上 "1"<Count :start="1"></Count>接收到才是数字 1

props 节点中定义的自定义属性可以和 data 中的属性一样直接在模板语法或方法中使用,但是自定义属性值不能修改。尽管修改可以成功,但是 Vue 会在控制台警告避免修改自定义属性,因为当父组件重新渲染时自定义属性值会被重写,也就是说自定义属性是“只读”的。正确的做法是把自定义属性的值赋值到 data 中的其他属性上再进行读写。

1
2
3
4
5
6
7
8
9
10
<script>
export default {
props: ["start"],
data() {
return {
count: this.start
}
}
};
</script>

props 节点的值除了是数组格式还可以是对象格式,对象格式中的每一个属性代表一个自定义属性值,可以通过下面选项进一步设置自定义属性:

  • default :定义自定义属性的在没有接收到值时默认值。
  • type :定义属性的值类型,如果类型不匹配也能接收,但是 Vue 会在浏览器控制台中警告。可选值有:StringNumberBooleanArrayObjectFunction 等。
  • required 选项将属性设置为必填项,强制其他组件在使用该组件时必须为自定义属性传递值(哪怕设定了 default)否则 Vue 会在浏览器控制台中警告。
1
2
3
4
5
6
7
8
9
10
11
12
<script>
export default {
props: {
start: {
default: 0,
type: Number,
required: true,
}
}

};
</script>

生命周期

组件的生命周期(Life Cycle)是指一个组件从创建到运行到销毁的整个阶段,强调的是一个时间段。

生命周期函数是由 Vue 框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行。

生命周期强调的是时间段,生命周期函数强调的是时间点。

image-20211128012917670

在一个组件的 <template> 中通过标签形式引用了其他 Vue 组件就相当于创建了一个组件实例,每一个组件实例实际上就是一个 Vue 实例。对于任何一个组件实例而言,阶段 1 和阶段 3 只会执行一次,阶段 2 中的函数最少执行零次,最多执行若干次。

各个阶段之间的具体差异看下图:

lifecycle

最佳实践:

  • Ajax 请求放在 created() 或者 beforeMount() 都可以,请求越早可以越快得到请求结果,因此建议放在 created() 中。
  • 如果要操作 DOM 元素,最早可以在 mounted() 中进行。
  • 当数据变化后,在 updated() 函数中操作最新的 DOM 元素。 更简单的做法是使用组件的 $nextTick(callback) 方法,会把 callback 回调函数推迟到下一个 DOM 更新周期之后执行。通俗的理解是:等组件的 DOM 更新完成之后,再执行 callback 回调函数。从而能保证在 callback 回调函数中可以操作到最新的 DOM 元素。

数据共享

两个 Vue 组件实例之间的可以有父子关系和兄弟关系。

image-20211128174939118

上图中组件 A 和 D 之间是父子关系,B 和 E 是之间兄弟关系。

父组件向子组件传递数据需要利用自定义属性,具体步骤如下:

  1. 在子组件中通过 props 节点声明自定义属性:

    1
    2
    3
    4
    5
    <script>
    export default {
    props: ["msg", "user"],
    };
    </script>
  2. 父组件使用子组件时通过 v-bind 指令给属性赋值,一定要使用 v-bind 指令而不是直接通过属性赋值,否则子组件接收到的是字符串,而不是真正的变量值。

    1
    <Son :msg="message" :user="userinfo"></Son>

注意:通过 props 传递的值如果是原始类型则是值传递,如果是复杂类型则是引用传递。这意味着在使用子组件时如果传递的是一个对象,如果在子组件中改变了对象的属性,父组件中的对象属性也会改变;在使用子组件时如果传递的是一个原始类型数据,在子组件中直接修改会控制台会报警告,因为自定义属性是“只读”的,转存到其他变量后可以修改,但也不会影响到父组件中的值,因为修改的是拷贝。

子组件向父组件传递数据需要利用自定义事件,具体步骤如下:

  1. 在父组件中使用 v-on 事件绑定指令为子组件实例的注册自定义事件,可以是函数名也可以是函数调用语句,并且必要时也可以使用 $event 变量显示传入事件对象。

    1
    <Son @shareValue="getValueFromSon"></Son>
  2. 子组件中需要传值的时候使用 $emit() 触发自定义事件,该函数第一个参数是要触发的事件名称,剩余的参数为事件对象,作为实参依次隐式传入事件处理函数,但是 $event 变量中只存储了剩余参数中的第一个参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <script>
    export default {
    data() {
    return {
    value: 1,
    };
    },
    created() {
    this.$emit("shareValue", this.value);
    },
    };
    </script>
  3. 在父组件的事件处理函数中通过形参可以接受到子组件传来的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <script>
    export default {
    methods: {
    getValueFromSon(val) {
    console.log(val);
    }
    }
    };
    </script>

兄弟组件之间的传递数据需要使用借助一个 Vue 实例作为中介,这种方案称作为 EventBus 事件总线。实际上任意两个组件都可以使用 EventBus 来共享数据,即使两个组件之间有父子关系但是层次嵌套很深也建议使用 EventBus 来共享数据。使用 EventBus 在组件之间数据共享的具体步骤如下:

  1. 创建 eventBus.js 文件,里面向外导出了一个不带任何 DOM 结构的 Vue 的实例对象,完整内容如下。

    1
    2
    import Vue from 'vue'
    export default new Vue()
  2. 在作为数据接收方的组件中导入在这个 EventBus 对象,(通常是在 created() 中)调用 $on() 方法为共享对象注册一个自定义事件,函数的第一个参数是自定义事件名称,第二参数是事件处理函数,事件处理函数中可以接受传递进来的参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script>
    imoprt bus from './eventBus.js'
    export default {
    created() {
    bus.$on('share', val => {
    console.log(val)
    })
    }
    }
    </script>
  3. 在作为数据发送方的组件中也导入这个 EventBus 对象,调用共享对象的 $emit() 触发自定义事件,该函数第一个参数是要触发的事件名称,剩余的参数作为实参传入事件处理函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <script>
    imoprt bus from './eventBus.js'
    export default {
    data() {
    return {
    value: 1,
    }
    }
    created() {
    bus.$emit('share', value)
    }
    }
    </script>

ref 引用

每个 Vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象,只有给组件中的 DOM 元素或者组件添加了 ref 属性才可以通过 $refs 对象的属性获取 DOM 元素或着组件的引用。

注意:如果两个 DOM 元素或者组件的 ref 属性值相同,则只能拿到后面元素的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<input type="text" ref="ipt"/>
<Son ref="son"></Son>
<button @click="getRef"></button>
</div>
</template>
<script>
import Son from "./Son.vue";
export default {
methods: {
getRef() {
console.log(this.$refs.ipt); // 原生 DOM 对象引用
console.log(this.$refs.son); // 子组件实例引用
},
},
components: {
Son,
},
};
</script>

动态组件

Vue 提供了一个内置的 <component> 组件,通过修改这个标签的 is 属性的值可以控制组件的显示与隐藏,从而实现组件的动态渲染。

1
<component is="Left"></component>

默认情况下,在切换动态组件 is 属性值时,切换动态组件无法保持组件的状态,显示出来的组件会被重新创建,所有的数据重新初始化,上一次显示的组件会被隐藏销毁。此时可以使用 Vue 内置的 <keep-alive> 组件保持动态组件的状态。

1
2
3
<keep-alive>
<component :is="comName"></component>
</keep-alive>

<keep-alive> 可以把隐藏的组件进行缓存,而不是直接销毁隐藏的组件,在 Vue 调试工具中有 inactive 标识表示这是未激活的缓存组件。

当组件被缓存时,会自动触发组件的 deactivated() 生命周期函数。当组件被激活时,会自动触发组件的 activated() 生命周期函数。

<keep-alive> 中的动态组件第一次被激活时,先执行 created() 生命周期函数,再执行 activated() 函数,当组件再次被激活的时候,因为组件被缓存,只会触发 activated() 函数,而不会触发 created()

默认情况 <keep-alive> 会把所有里面的所有动态组件缓存,可以使用 include(或 exclude)属性来指定(或排除)哪些组件被缓存,只有组件名称匹配的组件才会被缓存(或不被缓存),多个组件名之间使用英文的逗号分隔。

注意:includeexclude 属性不能同时使用,属性值匹配的是组件名称,而不是组件的注册名称。

如果在组件的默认导出对象中没有为提供 name 属性,则组件名称和 components 节点中的注册名称相同。如果指定了 name 属性值,name 属性值才是组件的真正名称。

1
2
3
4
5
6
// Left.vue
<script>
export default {
name: MyLeft
}
</script>

通过标签形式使用组件时用到的是组件的注册名称,在 Vue 调试工具中和通过 <keep-alive> 中的 includeexclude 属性筛选到的都是组件名称。

1
2
3
<keep-alive include="MyLeft,MyRight">
<component :is="comName"></component>
</keep-alive>

插槽

默认插槽

在封装组件时,把不确定的、希望由组件使用者指定的部分定义为插槽(slot),可以把插槽认为是组件中预留的占位符。

每个 <slot> 插槽区域都有 name 属性,如果没有指定 name 名称,会有隐含的名称叫做 default。组件内只定义了一个插槽时通常省略插槽的 name 属性。

1
2
3
4
5
6
7
// Article.vue
<template>
<!-- 默认插槽 -->
<slot></slot>
<!-- 和上面等效 -->
<slot name="default"></slot>
</template>

使用组件时可以为插槽指定具体的内容,组件标签中的所有内容会被填充到所有名字为 default 的默认插槽之中。但是如果在封装组件时里面没有定义任何 <slot> 插槽区域,即使为组件指定了自定义内容也会被丢弃。

1
2
3
<Article>
<p>文章正文</p>
</Article>

封装组件时,可以为占位的 <slot> 插槽提供预留的后备内容(默认内容)。如果使用组件时没有为插槽提供任何内容,则后备内容会生效。

1
2
3
4
5
6
7
// Article.vue
<template>
<!-- 默认插槽 -->
<slot>
<p>默认内容</p>
</slot>
</template>

具名插槽

如果在封装组件时预留了多个插槽节点,则需要为每个 <slot> 插槽通过 name 属性指定具体的插槽名称。这种带有具体名称的插槽叫做具名插槽。

1
2
3
4
5
6
7
// Article.vue
<template>
<!-- 头部插槽 -->
<slot name="header"></slot>
<!-- 默认插槽 -->
<slot name="default"></slot>
</template>

在使用带有具名插槽的组件时,需要通过 v-slot 指令指定要被替换的插槽的名称,v-slot 指令冒号后面的名称没有引号,带引号反而会报错。

注意 v-slot 只能添加在 <template> 上(只有一种例外情况可以用在组件标签上),因此要在外面包裹一个 <template> 标签。这里的 <template> 标签只起到简单的包裹的作用,不会和组件标签一样解析成 Vue 实例对象,也不会渲染成任何 DOM 元素。任何没有被包裹在带有 v-slot<template> 中的内容都会被视为默认插槽的内容

1
2
3
4
5
6
7
8
9
10
11
12
<Article>
<template v-slot:header>
<p>
文章标题
</p>
</template>
<template v-slot:default>
<p>
文章正文
</p>
</template>
</Article>

v-onv-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 v-slot: 替换为字符 #。例如上面的 v-slot:header 可以被缩写为 #headerv-slot:default 缩写为 #default,但是 v-slot:default 不能被缩写为 #

作用域插槽

在封装组件的过程中,可以通过预留的 <slot> 插槽的属性值绑定数据,这种插槽叫做作用域插槽,绑定在 <slot> 元素上的属性(除了 name 属性)被称为为插槽属性。通过作用域插槽可以实现子组件向父组件传递数据的效果。

注意:在绑定数据时如果直接给属性赋值则属性值以字符串传递,如果通过 v-bind 属性绑定则传递的 JavaScript 表达式的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Article.vue
<template>
<!-- 默认插槽 -->
<slot name="default" msg="Simple string." :user="user"></slot>
</template>
<script>
export default {
data() {
return {
user:{
name:'zs',
}
}
}
}
</script>

父组件在给作用域插槽指定内容时,可以通过 v-slot 指令的属性值来接收,接收对象会包含所有插槽属性。

注意:如果定义插槽时没有绑定任何插槽属性,父组件去接收是没有意义的,因为会得到一个空对象。

1
2
3
4
5
6
<Article>
<template v-slot:default="scope">
<p>{{scope.msg}}</p>
<p>{{scope.user.name}}</p>
</template>
</Article>

接收的对象命名可以任意,多取名为 scope 或者 slotProps,也可以在接收的同时解构赋值。

1
2
3
4
5
6
<Article>
<template v-slot:default="{msg, user: person}">
<p>{{msg}}</p>
<p>{{person.name}}</p>
</template>
</Article>

自定义指令

Vue 还允许开发者自定义指令,就像使用 v-textv-model等指令一样使用在标签上。

vue 中的自定义指令分私有自定义指令和全局自定义指令两类。

在每个 Vue 组件中,可以在默认导出对象的 directives 节点下声明私有自定义指令,指令名不包括 v- 前缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
directives: {
color: {
bind(el) {
el.style.color = 'red'
},
update(el, binding) {
el.style.color = binding.value
}
}
}
}
</script>

私有自定义指令可以同一个组件的 <template> 模板结构中使用,在使用自定义指令时,需要在定义的指令名前面加上 v- 前缀。

1
2
3
4
5
6
7
<template>
<div>
<!-- 指令名准确是 color 而不是 v-color,声明指令不需要 v- 使用时需要 -->
<p v-color="myColor"></p>
<p v-color="'pink'"></p>
</div>
</template>

指令第一次被绑定到元素上的时候会立即触发 bind(el, binding) 函数,在这里可以进行一次性的初始化设置。

当组件实例中的 data 数据的修改会导致组件 updated() 生命周期函数,同时也会调用自定义指令中的 update(el, binding) 函数。

上面两个函数中的第一个参数 el 表示指令所绑定到的原生 DOM 对象,第二个参数 binding 对象中存储者自定义指令的参数相关信息,binding 对象的 value 属性可以获取指令的参数值(即 = 后面的 JavaScript 表达式的最终值,例如:v-my-directive="1 + 1" 中,绑定值为 2。),binding 对象的 expression 是属性值表达式的原始值(即 = 后面的 JavaScript 表达式的原始字符串,字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1")。

注意:和 Vue 指令自定义指令的属性值本质上也是 JavaScript 表达式,如果自定义指定的参数值直接是一个简单字符串字面量则需要的额外包裹引号。

如果 bind()update() 函数中的行为完全相同,则定义自定义指令时可以简写成函数格式,函数名就是自定义指令名,函数体bind()update() 函数的行为。

1
2
3
4
5
6
7
8
9
<script>
export default {
directives: {
color(el, binding) {
el.style.color = binding.value
}
}
}
</script>

私有自定义指令只能在当前组件中使用,通用性很差。与全局组件和全局过滤器类似,在 Vue 项目的 main.js 入口文件中,通过 Vue.directive() 方法,可以注册全局自定义指令。该方法的第一个参数为字符串表示全局自定义指令的名字,第二个为参数为一个对象,里面指定 bind()update() 函数。

1
2
3
4
5
6
7
8
Vue.directive('color', {
bind(el) {
el.style.color = 'red'
},
update(el, binding) {
el.style.color = binding.value
}
})

同样,如果 bind()update() 函数中的行为完全相同,第二个参数可以简写成同一个函数。

1
2
3
Vue.directive('color-swatch', function (el, binding) {
el.style.color = binding.value
})

参考

  1. Vue 2 官方教程
  2. Vue事件总线(EventBus)使用详细介绍

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!