跳转至

第11章 Vue

第 11 章

Vue

本章要学习的 Vue 是一套用于构建用户界面的渐进式框架,同时也是一个 JavaScript 框架。任何编程语言在最初的时候都是没有框架的,随着在实际开发过程中不断总结经验,积累最佳实践,慢慢地,人们发现很多特定场景下的特定问题总是可以套用固定的解决方案。于是有人把成熟的固定解决方案收集起来,整合在一起就成了框架。在使用框架的过程中,我们往往只需要告诉框架做什么(声明),而不需要关心框架怎么做(编程)。

对于 Java 程序来说,我们使用框架就是导入那些封装了固定解决方案的 jar 包,然后通过配置文件告诉框架做什么,从而大大简化编码,提高开发效率。例如,JUnit 其实就是一款单元测试框架。而对于 JavaScript 程序来说,我们使用框架就是导入那些封装了固定解决方案的 JS 文件,然后在框架的基础上编写代码实现业务逻辑。

Vue 框架提供了一套开发规则,按照这个开发规则可提高开发效率。Vue 框架是轻量级的,有很多独立的功能或库,使用 Vue 时可以根据项目需求来选用它的一些功能。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。本章主要介绍 Vue 的基本语法和生命周期,以及如何应用,如声明式渲染、条件渲染、列表渲染、事件驱动等。

11.1 Vue 简介与入门案例

Vue 的作者是尤雨溪,Vue 最早发布于 2014 年 2 月。作者在 Hacker News、Echo JS 与 Reddit 的 JavaScript 版块发布了最早的版本。一天之内,Vue 就登上了这三个网站的首页。Vue 是 Github 上最受欢迎的开源项目之一。

Vue 最初的目标是成为大型项目的一个良好补充,“渐进式框架” 的设计思想是为了遵循淡化框架本身的主张,从而降低框架作为工具的复杂度,以及对使用者的要求。

Vue 是为了实现前后端分离的开发理念,开发前端 SPA(single page web application)项目,实现数据绑定、路由配置、项目编译打包等一系列工作的技术框架。Vue 有著名的全家桶系列,包含了 vue-router、vuex、vue-resource,再加上构建工具 vue-cli、sass 样式,就是一个完整的 vue 项目的核心构成。概括起来就是项目构建工具、路由、状态管理、HTTP 请求工具。

由于 Vue.js 只聚焦视图层,本质上说是一个构建数据驱动的 Web 界面的库。Vue 通过简单的 API(应用程序编程接口)提供高效的数据绑定和灵活的组件系统。Vue 的特点如下。

Vue.js 的两大核心要素是数据驱动和组件化。数据驱动,简单来说就是修改绑定的数据(页面上依赖的数据)就能对应地更新视图(页面),这样一来极大地解放了 DOM 操作的工作,提高开发效率。组件化开发通常是将一个应用以一棵嵌套组件树的形式来组织,把页面按照页面功能(如导航、侧边栏、下拉框)拆分业务,每个组件代表一个独立的功能,从而大大提高了代码可维护性和复用性。

Vue 数据驱动采用 MVVM 模式,M(Model)指的是模型层,这里表示 JavaScript 对象,V(View)指的是视图层,这里表示 DOM(HTML 操作的元素),VM(ViewModel)指的是连接视图和数据的中间

件,Vue.js 就是 MVVM 中的 VM 层的实现者。Vue 与 MVVM 模式的关系如图 11-1 所示。

在 MVVM 架构中,是不允许数据和视图直接通信的,只能通过 VM 层来通信,而 VM 本质就是一个观察者。VM 能够观察到数据的变化,并对视图下对应的内容进行更新;还能够监听到视图的变化,并通知数据发生改变。综上,Vue.js 就是一个 MVVM 的实现者,它的核心就是实现了 DOM 监听和数据绑定。

18e9386b4971ae0d45ac1ca3e008c202f24fd87bd599d66854a8f810a85a2320.jpg

下面通过入门案例,来简单了解一下 Vue 的使用。

Vue 是 JavaScript 的框架,使用 Vue 开发需要引入该框架的源码,Java 的框架源码是 JAR 包形式,那么 JS 框架的源码是什么类型的文件呢?答案一目了然,就是 JS 文件。接下来以 Vue2.7.14 为例进行演示。首先搭建 Vue 框架,具体步骤如下。

搭建完成后的 HTML 文件如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="box">
    </div>
    <script>
    new Vue();
    </script>
</body>
</html> 

例如,创建一个 span 标签和一个按钮,单击按钮修改 span 标签的内容。接下来分别用 JavaScript 代码和 Vue 代码实现该案例效果。

使用 JavaScript 实现的代码如下。

<!DOCTYPE html>
<html lang="en"> 
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>
    function fun01() {
    var span01 = document.getElementById("span01");
    span01.innerHTML = "新的值";
    }
    </script>
</head>
<body>
    <!--创建一个 span 标签和一个按钮,单击按钮修改 span 标签的内容-->
    <span id="span01">这是原始内容</span><br/>
    <input type="button" value="单击修改 span 的值" onclick="fun01()">
</body>
</html>

查看页面效果,如图 11-2 所示。

单击该按钮,发现值发生变化,如图 11-3 所示。

56a5f2e90ab3f7aebed1357d3b08b511fbaae0135844f3bf665ffe2324cc8a08.jpg

999f3e12582a48241b4effb7b12920361439dccf701fc8e50e8b3fb002438f60.jpg

使用 Vue 实现的代码如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="vue.js"></script>
</head>
<body>
    <!--创建一个 span 标签和一个按钮,单击按钮修改 span 标签的内容-->
    <div id="box">
    <!--将 span 的标签体内容和 Vue 的数据模型做一个绑定关系,一旦绑定成功,要想操作 span 的内容,直接操作 msg 的值即可-->
    <span>{{msg}}</span><br/>
    <input type="button" value="按钮" @click="fun01()">
    </div>
    <script>
    //向 Vue 对象内传递 Json 对象
    new Vue({
    //el 是 Element 的缩写,#box 是 id 选择器,选择一个 Vue 可以操作的区域
    el: "#box",
    //data 是数据模型
    data: {
    msg: "这是原始数据"
    },
    //创建函数
    methods: {
    fun01:function () {
    //操作 span 标签的内容
    this.msg = "新值"
    }
    }
    }
}
}
});
</script>
</body>
</html> 

查看页面单击按钮,同样能实现相同的效果。Vue 的实现原理是将 span 的标签体内容和 Vue 的数据模型做一个绑定关系,一旦绑定成功,要想操作 span 的内容,直接操作 msg 的值即可。显而易见,如果页面内有很多位置,都需要操作 span 内容时,Vue 代码比 JS 更方便。

11.2 模板语法

随着前端交互复杂度的不断提升,各种字符拼接、循环遍历、DOM 节点的操作也都复杂多变,当数据量大,交互频繁的时候,不论是从用户体验还是性能优化上,都会面临前端渲染的问题。

Vue 使用了一种基于 HTML 的模板语法,使我们能够声明式地将应用或组件实例的数据绑定到呈现的 DOM 上。模板中除了 HTML 的结构,还包含两个 Vue 模板的特有语法:插值语法和指令语法。插值语法只有一个功能,就是向标签体插入一个动态的值。指令语法用来操作所在的标签,比如指定动态属性值、绑定事件监听、控制显示隐藏等。

在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态数据变更时,Vue 能够自动更新需要变化的 DOM 节点,并做到最小化更新。

下面分别对插值和指令进行具体讲解。

11.2.1 插值语法

插值语法是用来向标签体插入一个动态的数据值。插值语法的结构很固定,是用双大括号包含一个 JavaScript 表达式,像这样:{{JavaScript 表达式}}。值得一提的是,插值语法只可以作为一个标签的标签体文本或者文本的一部分,不能作为标签的一个属性值。双大括号中可以包含任意 JavaScript 表达式,除此之外,它包含的也可以是一个常量值、一个变量,还可以是一个变量对象的方法调用,甚至可以是一个三目表达式。

但是需要注意的是,模板中变量读取数据的来源都是配置指定的 data 对象。虽然还有其他的数据来源,在这里我们暂时只需要理解为 data 对象。

例如,来看一段简单的 Vue 使用实例。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
    <p>{{123}}</p>
    <p>{{msg}}</p>
    <p>{{msg.toUpperCase()}}</p>
    <p>{{score<60 ? '入学测试未通过暂时不可以来尚硅谷学习':'入学测试通过欢迎来尚硅谷学习'}}
    </p>
</div>
<script>
//向Vue对象内传递Json对象
new Vue({
    //el是Element的缩写,#app是id选择器,选择一个Vue可以操作的区域
    el: "#app",
    //data是数据模型
    data: {
    msg: 'Welcome to Atguigu',
    score: 59
    }
});
</script>
</body>
</html>
123   
Welcome to Atguigu   
WELCOME TO ATGUIGU   
入学测试未通过,暂时不可以来尚硅谷学习

上面代码使用插值语法分别包含了常量 “123”、变量 “msg”、变量对象的方法调用 “msg.toUpperCase ()”,以及三目表达式 “score<60?‘入学测试未通过,暂时不可以来尚硅谷学习’:‘入学测试通过,欢迎来尚硅谷学习’”。由于模板中的所有变量读取的都是 data 中的数据,除了常量 “123” 不需要读取数据,后三者都会读取 data 中对应的值。此时页面中输出的效果如图 11-4 所示。

11.2.2 指令语法

指令(Directives)是带有 “v-” 前缀的标签属性,其属性值一般是一个 JavaScript 表达式。Vue 中包含了一些不同功能的指令,比如 v-bind 用来给标签指定动态属性值,v-on 用来给标签绑定事件监听,v-if 和 v-show 用来控制标签是否显示。但要注意,不管是什么功能的指令,它们操作的都是指令属性所在的标签。

下面以 v-bind 与 v-on 为例来演示一下指令语法的使用,v-bind 与 v-on 两个属性的语法格式如下。

v-bind: 属性名="JS 表达式"
v-on: 事件名="方法名表达式"

示例代码如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
    <a v-bind:href="url">去学习IT技术</a><br/>
    <button v-on:click="confirm">确认一下</button>
    </div>
<script>
    //向Vue对象内传递Json对象
    new Vue({
    //el是Element的缩写,#app是id选择器,选择一个Vue可以操作的区域
    el:"#app",
    //data是数据模型
    data:{    url:'http://www.atguigu.com'
    },
    }
</body>
</html>
//创建函数
methods: {
    //单击 button 按钮触发 confirm 函数(方法)
    confirm:function () {
    if (window.confirm('确定要来学习吗?')) {
    window.location.href = 'http://www.atguigu.com'
    }
    }
    });
</script>
</body>
</html> 

上面这段代码使用了 v-bind 指令为 <a> 标签指定了动态属性值 url,此时 <a> 标签的 href 的值就是 data 中定义的 url 的值,上面代码还在 <button> 标签上使用了 v-on 指令,为其绑定监听并指定回调 confirm 函数,当单击按钮时触发该函数执行对应操作。

单击 F12 键,可以看到 <a> 标签的 href 属性值被替换成了 data 中定义的动态值,如图 11-5 所示。单击 “button” 按钮,先弹出如下对话框,如图 11-6 所示。

bd7e5b2bd0bbbd44946a8df236eebfb4415ce4ef9178417fb7c9e4b6878a5cef.jpg

f9c4406609338a8314c47e6cd2d54d20794709b5c62590cebbc0787efa400008.jpg

然后单击 “确定” 按钮后跳转尚硅谷官网,如图 11-7 所示。

b0b38ec75ecc1d9960e8e80d582765846989ea299fde4d8b4c0b53eea6266c68.jpg

Vue 允许将 “v-bind: 属性名” 简化为 “: 属性名”、“v-on: 事件名” 简化为 “@事件名” 的形式。此时,我们可以将上面代码简化为如下代码。

<div id="app">
<a :href="url">去学习 IT 技术</a><br>
<button @click="confirm">确认一下</button>
</div>

11.2.3 data 属性和 methods 方法

从入门案例中看出,在创建 Vue 对象时,其内部包含几个属性,这里重点介绍一下 data 和 methods。

data 的返回值是一个包含 n 个可变属性的对象,一般称此对象为 data 对象。此对象中的属性,即称为 data 属性。模板中可以读取任意 data 属性进行动态显示。

而 methods 是一个包含 n 个方法的对象。在方法中可以通过 this.xxx 来读取或更新 data 对象中对应的属性。当更新了对应的 data 属性后,界面会自动更新。

有一点需要特别说明,data 对象和 method 方法中的 this 本质上是一个代理(Proxy)对象。它代理了 data 对象所有属性的读写操作,也就是说,我们可以通过 this 来读取或更新 data 中的属性。在 methods 中定义的所有方法最终也添加到了代理对象中,同理也可以在方法中通过 this 来更新 data 属性,从而触发界面自动更新。至于模板中的表达式读取变量或函数,本质上也都是从代理对象上查找的。

11.3 声明式渲染

声明式渲染是指使用简洁的模板语法,声明式的方式将数据渲染进 DOM 系统。声明式是相对于编程式而言,声明式是面向对象的,告诉框架做什么,具体操作由框架完成。编程式是面向过程思想,需要手动编写代码完成具体操作。渲染通俗来讲就是,将 Vue 数据模型中的值显示在网页上的过程。

例如,使用 Vue 模板语法 “{{message}}” 来声明式地将 message 数据显示在网页上,而使用模板语法的这种方式就叫作声明式渲染。HTML 文档渲染前后的效果如图 11-8 所示。

5af6fca098a906182847c4f47b70dbdb3720a1c64a8ece2e864cfeb20b28c3c2.jpg

其中,方框表示 HTML 标签。空心圆表示动态、尚未确定的数据。实心圆表示经过程序运算以后,计算得到的具体的、可以直接在页面上显示的数据。渲染就是程序计算动态数据得到具体数据的过程。下面看一个具体的例子。

HTML 代码如下。使用插值表达式 “{{message}}”,指定要被渲染的数据。

<!-- 使用{{}}格式,指定要被渲染的数据 -->
<div id="app">{{message}}</div>

Vue 代码如下。创建 Vue 对象,并传入所需要的参数。

//创建Vue对象,挂载#app这个div标签
var app = new Vue({
    el: "#app",
    // data属性设置了Vue对象中保存的数据
    data: {
    message: "Hello Vue!"
    }
});

或者分两步创建,首先创建 JSON 对象,指定 Vue 对象要关联的 HTML 元素,以及 message 对应的具体数据,然后将该 JSON 对象作为参数传入 Vue 对象。

var argumentJson = {
    el: "#app",
    // data 属性设置了 Vue 对象中保存的数据
    data: {
    message: "Hello Vue!"
    }
};
// 2. 创建 Vue 对象,传入上面准备好的参数
var app = new Vue(argumentJson);

通过验证 Vue 对象的响应式效果,可以看到 Vue 对象和页面上的 HTML 标签始终保持着关联的关系,同时可知 Vue 框架在背后也做了大量的工作。

接下来,我们继续介绍不同类型的数据渲染,包括标签绑定、条件渲染、列表渲染、事件驱动等。

11.4 标签绑定

Vue 数据模型 data 与标签绑定包括两种:与标签体内容绑定和与标签属性值绑定。Vue 的数据模型和标签体内容进行绑定,只发生在双标签上,通常借助插值表达式绑定,语法格式如下。

<标签名> 插值表达式:{{数据模型中定义的 key 值}}</ 标签名>

示例代码参见 11.2.1 插值语法的应用。

而 Vue 的数据模型和标签的属性值进行绑定,可以发生在双标签,也可以发生在单标签,包括单向绑定和双向绑定两种。

11.4.1 单向绑定

单向绑定通常借助指令语法 v-bind 实现,语法格式如下。

v-bind: 原始属性名 = “数据模型中定义的 key 值”

下面通过具体的例子进行演示。

在 HTML 代码如下,为 <input> 标签中 value 属性绑定 Vue 数据模型 data 中定义的 msg 值。

<div id="app">
    <!-- v-bind:value 表示将 value 属性交给 Vue 来进行管理,也就是绑定到 Vue 对象 -->
    <!-- msg 是一个用来渲染属性值的表达式,相当于标签体中加{{}}的表达式 -->
    <input type="text" v-bind:value="msg" />

    <!-- 同样的表达式,在标签体内通过{{}}告诉 Vue 这里需要渲染; -->
    <!-- 在 HTML 标签的属性中,通过 v-bind:属性名="表达式"的方式告诉 Vue 这里要渲染 -->
    <p>{{msg}}</p>
</div>

在 Vue 代码如下,创建 Vue 对象,指定 msg 对应的值。

//创建 Vue 对象,挂载#app 这个 div 标签
var app = new Vue({
    el: "#app",
    data: {
    msg: "太阳当空照"
    }
});

单向绑定页面效果如图 11-9 所示。

7154be8c422721592786b556c11eee38ba8f61e33c0cd7c8a9a9fdc3bb688da4.jpg

值得一提的是,v-bind 可以省略,其语法格式简化为如下形式。

//省略 v-bind

: 原始属性名 =“数据模型的 key 值”

与 value 绑定类似,style 绑定同样是通过 “v-bind:style="数据模型中定义的 key 值"” 来绑定动态 style 样式,当然我们一般也会使用 “:style="数据模型中定义的 key 值"” 的简写方式。

HTML 代码如下。

<div id="app">
    <p v-bind:style="fontCss">Vue and CSS</p>
</div> 

Vue 代码如下。

//创建 Vue 对象
new Vue({
    el: "#app",
    data: {
    fontCss: {
    color: "red",
    font-size: "30px",
    }
    }
}); 

另外,对于 value 属性值的绑定,我们需要思考一个问题,当修改 Vue 数据模型中 msg 的值时,对应 <input> 标签中的 value 属性值会随之改变,那么用户修改文本框的 value 属性时,msg 的值会跟着变化吗?答案是并不会改变。修改数据模型的值,网页会跟着发生变化,反之不变,我们把这种现象称为单向绑定。那么如何实现双向绑定呢?我们继续往下看。

11.4.2 双向绑定

双向绑定需要借助另一个指令语法 v-model 实现,语法格式如下。

v-model: 原始属性名 = “数据模型的 key 值”

当然,并不是所有的标签属性都能够双向绑定,只有用户可以修改的属性能够满足双向绑定。例如,表单项里标签的 value 属性,用户是可以修改的,可以进行双向绑定,而 id、name 等属性都不适用。

同样地,将前面单向绑定中的示例,使用双向绑定重新实现。HTML 代码如下。

<div id="app">
    <!-- v-bind:属性名 效果是从Vue对象渲染到页面 -->
    <!-- v-model:属性名 效果不仅是从Vue对象渲染到页面,而且能够在页面上数据修改后反向修改Vue对象中的数据属性
-->
<input type="text" v-model:value="msg" />
<p>{{msg}}</p>
</div>

Vue 代码如下。

// 创建 Vue 对象,挂载#app 这个 div 标签
var app = new Vue({
    el: "#app",
    data: { 
msg:"太阳当空照"
}
); 

双向绑定的页面效果如图 11-10 所示。

可以看出,<p> 标签内的数据能够和文本框中的数据实现同步修改。这也说明,页面上数据被修改后,Vue 对象中的数据属性也跟着被修改。

另外,v-model 还可以省略属性名,即默认 value 属性,简化后的语法格式如下。

//去掉属性名,默认为 value 属性
v-model = “数据模型的 key 值”

b70837a43ff35cdacdcfc2d1b6670299774a9b89719949264a2468fda810d862.jpg

在实际开发中,要考虑到用户在输入数据时,有可能会包含前后空格。而这些前后的空格对于程序运行来说都是干扰因素,需要去掉不必要的空格。

我们可以通过在 v-model 后面加上.trim 修饰符来实现去空格。

//去掉前后空格
v-model.trim=数据模型的 key 

11.5 条件渲染

在项目开发中,有两种场景极为常见:第一种局部界面需要满足某种条件后显示,第二种是在多个不同的界面效果中间进行切换显示。无论哪一种,都需要满足某种条件才能显示渲染。在 Vue 中将其称为条件渲染,可以通过 v-if 相关指令或 v-show 指令实现。

11.5.1 v-if 相关指令

v-if 指令用于控制元素显示或者隐藏,其相关指令包含 v-if、v-else 和 v-else-if。v-if 与 v-else-if 指令的表达式需要一个布尔型的表达式,结果是 true,表示满足条件,显示对应的元素;v-else 是不需要指定表达式的,只需要指定属性名,当其前面的 v-if 和 v-else-if 指令的表达式都不满足时才会显示。

如果只有一个界面的条件渲染,推荐选择使用 v-if 指令。如果有两个界面的条件渲染,推荐选择使用 v-if 与 v-else 指令。如果超过两个界面的条件渲染,那么推荐选择使用 v-if、v-else-if 和 v-else 指令来实现。

阅读下方代码并思考下面代码的运行效果,分析如何在下面代码的基础上进行优化。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
    <h1>你的入学测试得分:{{score}}</h1>
    <button @click="examAgin">学习后再复试一次</button>
    </div>
    <script>
    //创建 Vue 对象,挂载#app 这个 div 标签
    var app = new Vue({
    el: "#app",
    data: {
    score: 55,
    },
    }
)
methods: {
    examAgin () {
    this.score += 10
    }
}
});
</script>
</body>
</html> 

这段代码比较简单,页面上会显示 data 中的 score 的值和一个按钮,当单击按钮后,score 的值增加 10。如图 11-11、图 11-12 所示。

394397aa3d4bd270cd66052a271abc713e0cd8db7dac37614697d8638e48c002.jpg

e4085f4d349b6a45692de0f943d6731ad7aa24f9c79dd3e21db1af64f05ba451.jpg

但如果现在的需求改为:只有当入学得分小于 60 的时候才会显示该按钮。此时我们就可以通过 v-if 指令来实现。在 <button> 按钮上做简单改动,具体如下。

<button @click="examAgin" v-if="score<60">学习后再复试一次</button> 

你的入学测试得分: 65

当 v-if 指定的表达式的值为 true 时,<button> 按钮才会显示。对于上面代码来说,初始时 score 的值为 55,显示按钮,单击按钮后,由于 score 大于 60 了,所以按钮自动消失了,如图 11-13 所示。

图 11-13 得分大于 60 时按钮消失

现在再次添加一个新需求:当得分大于等于 60 时,显示绿色的 h3 文本 “欢迎来到尚硅谷学习!”;当得分小于 60 时,显示红色的 h4 文本 “很遗憾,你的入学测试没有通过...”。

根据前面的学习,相信大家已经想到使用 v-if 对两个标签进行控制。但实际上,Vue 提供了更便捷的方式,前面提及过 v-if 的相关指令,对于这个需求可以使用 v-if 和 v-else 配合实现。不过需要注意,v-else 不用指定表达式。

此时修改代码,具体如下。

<h2 v-if="score>=60" style="color: green;">欢迎来到尚硅谷学习!</h2>
<h3 v-else style="color: red;">很遗憾,你的入学测试没有通过...</h3>

v-if 的条件与上一段代码的页面效果相同,这里不做过多讲解,但当不满足 v-if 的条件时,就会显示 v-else 所对应的标签体内容。当 data 中的 score 值为 55,不满足条件,则会显示 “很遗憾,你的入学测试没有通过...”,如图 11-14 所示。

单击按钮,得分变成 65,大于 60,显示 “欢迎来到尚硅谷学习!”,如图 11-15 所示。

688af0441e45fd2aaadf67fc543c6124a872c88b079d9624dad491dd6e8f907b.jpg

e62c125ab1f1f851db2011bb245e2931dd90d88e31764d821dfcbf984ad66d79.jpg

用户的需求总是多变的。如果需求再次更改:当得分大于等于 60 时,显示绿色的 h2 文本 “欢迎来到尚硅谷学习!”;当得分小于 50 时,显示红色的 h3 文本 “很遗憾,你的入学测试没有通过,可以考虑 UI 或测试”;如果在 50\~60,则显示紫色的 h4 文本 “此次测试没有完全通过,可选择复试”。此时可以使用 v-if、v-else-if 和 v-else 配合使用实现。需要注意的是,v-else-if 指定的表达式为 true 时,其对应的标签体内容才会显示。

再次修改代码,如下所示。

<button @click="examAgin" v-if="score<60 && score>=50">学习后再复试一次</button> 
<h2 v-if="score>=60" style="color: green;">欢迎来到尚硅谷学习!</h2>
<h3 v-else-if="score<50" style="color: red;">很遗憾,你的入学测试没有通过,可以考虑UI或测试
</h3>
<h4 v-else style="color: purple;">此次测试没有完全通过,可选择复试</h4>

当 score 的值大于等于 60 时,显示 “欢迎来到尚硅谷学习!”;当 score 的值小于 50 时,会显示 “此次测试没有完全通过,可选择复试”;其余情况显示 “很遗憾,你的入学测试没有通过,可以考虑 UI 或测试”。分别如图 11-16、图 11-17 和图 11-18 所示。

63d0982f0a56891b757eac7838b9aa2ab604998ed75e357ca556f592f7d9cf9e.jpg

c1a9589636ea21555069a19470d50cb1813108899a66845c919a2150b2acac20.jpg

8a1f333edf6b8b389dd9a9201ddacc52194195fc042605d280c292b7c05c556e.jpg

前面介绍的案例都是针对单个标签进行条件渲染,那么如果是多个标签又要怎么处理呢?我们可以使用 <template> 标签来包含需要条件渲染的多个标签,并在 <template> 标签上使用 v-if 指令来判断是否显示。需要注意的是,<template> 标签只存在于模板中,并不会在页面中产生任何标签与之对应,如下所示。

<h1>你的入学测试得分:{{score}}</h1>
<button @click="examAgin">学习后再复试一次</button><br/>
<template v-if="score>=60">
    <button>去找咨询老师办理入学</button><br />
    <button>去找技术老师深入交流技术疑问</button><br />
    <button>去找就业老师了解就业信息</button><br />
</template> 

运行代码后,由于 score 的值初始为 55,不满足 v-if 的条件,所以只显示入学得分和 “学习后再复试一次” 的按钮。当单击 “学习后再复试一次” 按钮后,score 的值更改为 65,满足 v-if 的条件,显示其余三个按钮,如图 11-19 所示。

此时观察页面结构,发现没有 <template> 标签,验证了 <template> 标签只在模板中存在,并不会产生任何对应的 HTML 标签的说法,如图 11-20 所示。

351e92bfc038fa1330ac36e28844f9be6a7d518bb7759f525dbe868de40c88e3.jpg

be5b5ec91d90d0fdbf8457af2a7e2faaab0b6f7beed544473e1d71388063f937.jpg

11.5.2 v-show

在 Vue 中,v-show 指令也可以实现条件渲染。当指定的表达式为 true 时,对应的内容才会显示。下面我们将 v-if 中涉及的案例再次使用 v-show 进行实现。

代码片段 1 如下。

<button @click="examAgin" v-show="score<60">学习后再复试一次</button> 

当 score 的值小于 60 时,显示 “学习后再复试一次” 按钮。

代码片段 2 如下。

<h2 v-show="score>=60" style="color: green;">欢迎来到尚硅谷学习!</h2>
<h3 v-show="score<60" style="color: red;">很遗憾,你的入学测试没有通过...</h3>

当 score 的值大于等于 60 时,显示 “欢迎来到尚硅谷学习!”;当 score 的值小于 60 时,显示 “很遗憾,你的入学测试没有通过...”。

代码片段 3 如下。

<h2 v-show="score>=60" style="color: green;">欢迎来到尚硅谷学习!</h2>
<h3 v-show="score<50" style="color: red;">很遗憾,你的入学测试没有通过,可以考虑UI或测试
</h3>
<h4 v-show="score<60 && score>=50" style="color: purple;">此次测试没有完全通过,可选择复试
</h4>

当 score 的值大于等于 60 时,显示 “欢迎来到尚硅谷学习!”;当不满足大于等于 60 和小于 50 时,会显示 “此次测试没有完全通过,可选择复试”;其余情况显示 “很遗憾,你的入学测试没有通过,可以考虑 UI 或测试”。

需要注意的是,v-show 是不能与 v-else 和 v-else-if 配合使用的。当需要在多个标签间切换显示时,要使用多个 v-show 来实现。还有一点,v-show 不能用在 <template> 标签上。

11.5.3 比较 v-if 和 v-show

通过前面学习的内容可知,v-if 和 v-show 两者实现的功能效果是一致的,但实际上二者内部的实现机制截然不同,这一点在理论面试时也是经常提问到的点,因此对于两者的区别,大家也需要了解一二。

在初始化解析显示时,如果表达式为 true,那么二者的内部处理相同;如果表达式为 false,处理完全不同。v-if 对应的模板标签结构不会解析,也就不会产生对应的 HTML 标签结构;而 v-show 则会解析模板,生成 HTML 标签结构,只不过会通过指定样式 display 为 none 来隐藏标签结构。

在更新数据后,表达式变为 true 时,就需要显示标签结构。v-if 的标签会重新创建 HTML 标签结构并显示,而 v-show 只需要去除 display 为 none 的样式让元素重新绘制出来。

当再次更新数据后,表达式变为 false 时,需要隐藏标签结构。v-if 的标签直接被删除,内存中不再有对应的 DOM 结构;v-show 的标签则通过 display 为 none 来隐藏标签结构。

接下来通过代码进行验证。

HTML 代码如下。

<div id="app">
    <h1 v-if="isShow">尚硅谷 IT 教育 1</h1>
    <h2 v-show="isShow">尚硅谷 IT 教育 2</h2>
    <button @click="isShow=!isShow">切换标识</button>
</div>

Vue 代码如下。

//创建 Vue 对象,挂载#app 这个 div 标签
var app = new Vue({
    el: "#app",
    data: {
    isShow: false
    }
});

代码中通过 v-if 指令控制 <h1> 标签的显示或隐藏,通过 v-show 来控制 <h2> 标签的显示或隐藏。

初始显示时,由于 isShow 为 false,页面中 <h1> 与 <h2> 对应的两个标签体都不可见。查看元素可以发现 <h1> 标签确实不存在,但 <h2> 标签是存在的,只是通过 display 为 none 的样式将其隐藏了,如图 11-21 所示。

单击按钮对 isShow 标识进行取反,isShow 更新为 true。此时 <h1> 和 <h2> 对应的两个标签体都会显示,但通过 v-if 控制的 <h1> 标签是新创建的,而 <h2> 标签是通过删除 display 为 none 的样式将其显示,如图 11-22 所示。

2c6be947f63c30054802d1425798b8306ae8ec4968a41253ff7008b600f549d6.jpg

3a6b1671f7a004d3673dbc65f05c9fd8d7e43b1f336a8d03f78f07d7191396ab.jpg

再次单击按钮时,isShow 更新为 false。此时 <h1> 和 <h2> 对应的两个标签体都会消失,但 v-if 的 <h1> 标签被删除,不存在了,但通过 v-show 控制的 <h2> 标签还存在,只是添加了 display 为 none 的样式。

最后简单总结 v-if 与 v-show 的区别,在隐藏时,v-if 会删除标签,而 v-show 只是通过样式控制标签不显示;再次显示时,v-if 需要重新创建标签,而 v-show 只需要更新样式就可以显示。

对于使用场景的选择上,如果条件渲染的条件变化相对频繁,或要控制的标签结构比较大,那么选择 v-show 比较合适。因为 v-if 在从隐藏变为显示时,需要重新创建 DOM 结构,效率相对低些。但 v-show 在隐藏时,标签结构没有删除,比 v-if 占用的内存更大一些。这也就是编程中经常说到的,以空间换时间的技术,即用更大的空间占用,换取后面更少的时间执行”。

还有一点需要注意,如果初始渲染条件值为 false 时,v-if 是不会解析模板产生 HTML 标签的,有时也称它为懒加载,但 v-show 则不是懒加载。

11.6 列表渲染

在项目开发的界面中,有很多列表效果需要动态显示。这在 Vue 中称为列表渲染,可以通过 v-for 指令来实现。通过 v-for 指令可以遍历多种不同类型的数据,数组是较为常见的一种数据,当然数据还可以是对象或数值。

动态显示列表后,我们可以对列表进行添加、删除、修改、过滤、排序等操作。另外,在通过 v-for 进行列表渲染时,有一个特别重要的属性 ——key 属性,这点需要注意。

11.6.1 列表的动态渲染

下面分别介绍 v-for 作用于数组、对象,以及整数的具体操作。

- 通过 v-for 遍历数组显示列表,其语法格式如下。

v-for=" (item, index) in array" 

其中,item 为遍历 array 数组的元素别名,index 为数组元素的下标。如果遍历不需要下标,可以简化为如下格式。

v-for="item in array"

另外值得注意的是,item 和 index 的名称不是固定的,可以自定义其他合法名称。

- 通过 v-for 遍历一个对象时,遍历的是对象自身可遍历的所有属性,其语法格式如下。

v-for=" (value, name) in obj" 

其中,value 为遍历的 obj 对象的属性值,而 name 是属性名。如果只需要属性值,可以简化为如下格式。

v-for="value in obj"

同样,value 和 name 的名称不是固定的,可以使用其他合法名称。

- 当 v-for 的目标是一个正整数 n 时,一般用于将当前模板在 1~n 的取值范围内重复产生 n 次,其语法格式如下。

v-for="value in n"

其中,value 为从 1 开始到 n 为止,依次递增的数值。接下来,通过代码分别演示 v-for 作用于 persons 数组、car 对象,以及 num 数字的 3 种情况。

HTML 代码如下。

<div id="app">
    <h2>测试遍历数组<人员列表>(用得很多)</h2>
    <ul>
    <li v-for="(p,index) in persons">
    {{index}}--{{p.name}}--{{p.age}}
    </li>
    </ul>
    <h2>测试遍历对象<汽车信息>(用得少)</h2>
    <ul>
    <li v-for="(value,name) in car">
    {{name}}--{{value}}
    </li>
    </ul>
    <h2>测试遍历指定次数(用得少)</h2>
    <ul>
    <li v-for="value in num">
    {{value}}
    </li>
    </ul>
</div> 

Vue 代码如下,提供 persons 数组、car 对象,以及 num 数值的信息。

// 创建 Vue 对象,挂载 #app 这个 div 标签

var app = new Vue({
    el:"#app",
    data: {
    persons: [
    {id:'001',name:'张三',age:22},
    {id:'002',name:'李四',age:24},
    {id:'003',name:'王五',age:23}
    ],
    car: {
    name:'奥迪A8',
    price:'70万',
    color:'黑色'
    },
    num: 5
    }
}); 

v-for 作用于单个标签的页面效果,如图 11-23 所示。

5278a02dce1b30118c800278591eb6b9c91307bf6411fec2a75dfecfe5dc5f30.jpg

与 v-if 类似,我们也可以利用在 <template> 标签上使用 v-for 指令,实现对多个标签的列表渲染。示例代码如下。

<div>
<h2>v-for 同时作用于多个标签</h2>
<ul>
    <template v-for="p in persons">
    <li>{{p.name}}--{{p.age}}</li>
    <span>来自尚硅谷学员</span>
    </template>
</ul>
</div> 

上面代码利用了在 <template> 标签上使用 v-for 的方式,实现了标签列表循环,此时页面中每个 <li> 标签和 <span> 标签都是一组,如图 11-24 所示。

综上,我们已经可以利用 v-for 指令对数据进行循环了。

11.6.2 列表的增删改

动态显示数组列表后,一般不会只作为展示存在,可能还需要对列表进行增删改操作。下面展示一个简易的列表,并演示对列表的增删改操作。如图 11-25 所示,用户对于该列表有如下需求。

v-for 同时作用于多个标签

07058e839c3e087b3379b5a7eb115d58f17c5316bb8ce586da0039585ad9426f.jpg

根据上述用户需求,编写代码实现其功能。

HTML 代码如下。

<div id="app">
    <h2>测试列表的增删改</h2>
    <ul>
    <li v-for="(p,index) in persons" :key="p.id" style="margin-top: 5px;">
    {{p.name}}--{{p.age}}--
    <button @click="deleteItem(index)">删除</button>--
    <button @click="updateItem(index, {id: Date.now(), name: '小兰', age: 12})">更新</button>
    </li>
    </ul>
    <button @click="addFirst({id: Date.now(), name: '小明', age: 10})">向第一位添加</button>
    -- 
    <button @click="addLast({id: Date.now(), name: '小红', age: 11})">向最后一位添加</button>
</div> 

Vue 代码如下。

//创建 Vue 对象,挂载#app 这个 div 标签
var app = new Vue({
    el: "#app",
    data: {
    persons: [
    {id: '001', name: '张三', age: 19},
    {id: '002', name: '李四', age: 18},
    {id: '003', name: '王五', age: 20}
    ]
    },
    methods: {
    addFirst (newP) { // 向第一位添加
    this.persons.unshift(newP)
    },
    addLast (newP) { // 向最后一位添加
    this.persons.push(newP)
    },
    deleteItem (index) { // 删除指定下标的
    this.persons.splice(index, 1)
    },
    updateItem (index, newP) { // 将指定下标的替换为新的人
    this.persons.splice(index, 1, newP)
    }
    }
});

上面代码中为每个按钮都封装了相应的方法来实现对应的功能,从而实现对数组元素的增删改操作。其实每个功能方法内部都是通过调用数组变更内部元素的方法(包括 unshift、push 和 splice 方法)来实现的。

11.7 事件驱动

前面已经多次演示在 <button> 按钮上绑定单击事件来处理单击响应操作了,本节我们详细介绍一下 Vue 中的事件处理。不过,Vue 中既支持原生 DOM 事件处理,也支持 Vue 自定义事件处理,这里我们暂时只关注原生事件的处理。

11.7.1 事件绑定方式

我们可以在 HTML 标签上使用 v-on 指令语法来绑定原生 DOM 事件,语法格式如下。

v-on:事件名="handler" 

通常情况下,我们一般用简化语法,具体如下。

@事件名="handler" 

handler 为事件处理器,Vue 中支持多种 handler 的写法。下面我们会对 handler 常用的两种写法进行讲解。

- handler 是一个方法名,对应 methods 配置中的一个同名方法。

- handler 是一条语句,此时这条语句有两种可能性,既可以是调用 methods 中某个方法的语句,也可以是直接更新 data 数据的语句。

例如,在如下初始代码的基础上,演示 handler 的使用。HTML 代码如下。

<div id="app">
    <h2>num: {{num}}</h2> 
<button>测试 event1(单击提示按钮的文本)</button><br><br>
<button>测试 event2(单击提示指定的特定数据)</button><br><br>
<button>测试 event4(单击 num 增加 3)</button><br><br>
<button>测试 event3(单击“提示”按钮的文本和指定的特定数据)</button><br><br>
</div>

Vue 代码如下。

//创建 Vue 对象,挂载#app 这个 div 标签
var app = new Vue({
    el: "#app",
    data: {
    num: 2
    },
});

1. handler 是一个方法名

当事件触发时,对应的方法就会自动调用。这也是使用最多的一种,我们可以使用它来实现第一个按钮的单击功能。

HTML 代码如下。

<button @click="handleClick1">测试 event1(单击提示按钮的文本)</button> 

Vue 代码如下。

//创建 Vue 对象,挂载#app 这个 div 标签
var app = new Vue({
    el:"#app",
    data: {
    num: 2
    },
    methods: {
    handleClick1 (event) {
    alert(event.target.innerHTML)
    }
    }
}); 

对于上面代码来说,当单击按钮时触发 handleClick1 方法。需要特别说明的是,handleClick1 方法中定义的形参 event,接收到的就是事件对象,这就是原生 DOM 事件监听回调原本的特性。

2. handler 是一条语句

handler 作为一条语句,包括调用 methods 中某个方法的语句和直接更新 data 数据的语句。下面具体说明。

(1)调用 methods 中某个方法的语句。

例如,借助 handler 作为调用方法的语句,并传入自定义数据,实现单击初始代码中的第二个按钮,提示指定的特定内容。

HTML 代码如下。

<button @click="handlerClick2('特定任意类型数据')">
测试 event2(指定特定数据)
</button>

Vue 代码如下。

//创建 Vue 对象,挂载#app 这个 div 标签
var app = new Vue({
    el: "#app",
data: {
    num: 2
},
methods: {
    handleClick2 (msg) {
    alert(msg)
    }
}
}); 

需要特别说明的是,方法调用的语句在事件发生前不会执行,只有在事件发生后,才会自动调用。

(2) 更新 data 数据的语句。

例如,借助 handler 作为直接更新 data 数据中 num 的语句,实现单击第三个按钮,让 num 的数量加 3。

HTML 代码如下。

<button @click="num += 3">测试 event4(单击 num 增加 3)</button>

11.7.2 事件的默认行为

比如单击超链接后会直接跳转页面、单击表单提交按钮后会直接提交表单等,是单击超链接和按钮后的默认行为。但是,如果现在我们希望单击超链接或按钮之后,根据判断的结果再决定是否要跳转,此时其默认行为就不符合我们的预期了,这种情况下,我们需要取消事件的默认行为。

在 JavaScript 代码中,可以调用事件对象的 preventDefault () 方法取消事件的默认行为。

(1) 取消超链接的默认行为。

HTML 代码如下。

<a id="anchor" href="http://www.baidu.com">超链接</a>

JavaScript 代码如下。

document.getElementById("anchor").onclick = function(event) {
    console.log("我单击了一个超链接");
    //取消单击超链接后的跳转行为
    event.preventDefault();
}

运行代码后发现,单击超链接并不会跳转百度网页。

(2) 取消表单提交的默认行为。

HTML 代码如下。

<form action="http://www.baidu.com" method="post">
    <button id="submitBtn" type="submit">提交表单</button>
</form>
JavaScript 代码如下
document.getElementById("submitBtn").onclick = function(event) {
    console.log("我单击了一个表单“提交”按钮");
    //取消单击提交按钮后的表单提交行为
    event.preventDefault();
}

ffefcdceb03ae0958fc15482831dbca404d805b8ed0abdc49c12c3d414ca7da2.jpg

运行代码后发现,填写完表单单击表单 “提交” 按钮后并不会跳转提交。

另外还有一个常见的事件冒泡行为。事件冒泡是指当一个元素上的事件被触发的时候,比如鼠标单击了一个按钮,同样的事件将会在那个元素的所有祖先元素中被触发。这一过程被称为事件冒泡。这个事件从原始元素开始一直冒泡到 DOM 树的最上层。

如图 11-26 所示,由两个 <div> 标签组成。

示例代码如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 引入样式 -->
    <link href="demo02.css" type="text/css" rel="stylesheet" />
</head>
<body>
    <div id="outerDiv">
    <div id="innerDiv"></div>
    </div>
    <!-- 引入JS文件 -->
    <script src="demo02.js"></script>
</body>
</html>

单击内部 <div> 标签的同时,也相当于单击了外层的 <div> 标签,此时如果两个 <div> 标签上都绑定了单击响应函数,那么就会同时被触发。demo02.js 文件如下。

document.getElementById("outerDiv").onclick = function() {
    console.log("外层div的事件触发了");
}
document.getElementById("innerDiv").onclick = function() {
    console.log("内层div的事件触发了");
} 

事件冒泡就是一个事件会不断向父元素传递,直到最顶层的 window 对象。如果这不是我们想要的效果,那么可以使用事件对象的 stopPropagation () 方法来阻止事件冒泡行为。

document.getElementById("innerDiv").onclick = function(event) {
    console.log("内层div的事件触发了");
    //取消事件冒泡行为
    event.stopPropagation();
} 

11.7.3 Vue 事件修饰符

在原生 DOM 事件处理中,我们可以在事件监听回调函数中通过 event.preventDefault () 来阻止事件默认行为,还可以通过 event.stopPropagation () 来阻止事件冒泡。Vue 主张在方法中最好是只有纯粹的数据逻辑,而不是处理 DOM 事件细节,因此设计了更简洁的事件修饰符,来实现对事件的这两个操作及事件的其他操作。

事件修饰符是用点表示的事件指令后缀,比如 “@事件名。修改符名 ="handler"”。常用的事件修饰符如下。

下面通过案例来演示几个常见事件修饰符的使用。

HTML 代码如下。

<div id="app">
    <a href="http://www.baidu.com" @click.prevent="test1">去百度</a>
    <br><br>
    <div style="width:200px;height:200px;background: yellow;" @click="test2">
    outer 
<div style="width:100px;height:100px;background:green;
margin-top: 30px;margin-left: 50px;"><click.stop="test3">
    inner
</div>
</div>
<br>
<button @click.once="test4">只响应一次单击</button>
</div> 

Vue 代码如下。

var app = new Vue({
    el:"#app",
    data: {
    num: 2
    },
    methods: {
    test1 () {
    alert('响应单击链接')
    },
    test2 () {
    alert('单击 outer')
    },
    test3 () {
    alert('单击 inner')
    },
    test4 () {
    alert('响应单击')
    }
    },
}); 

b76f6ed2b5cbe96023f8a487a9700b06bfd7ee155df73b12fb04e2afc9a3738a.jpg

演示事件修饰符的页面效果如图 11-27 所示。

通常情况下,单击链接默认会跳转页面,但是上面代码通过 “.prevent” 阻止默认行为,最终就不会跳转了。

在外部 div 和内部 div 上都绑定单击事件。当单击内部 div 的绿色区域,默认内部 div 会响应,但是由于事件冒泡,外部 div 也会响应。上面代码加上了 “.stop” 停止事件冒泡,那么外部 div 就不会响应了。

另外,因为上面代码为按钮指定了 “.once” 修饰符,所以单击下面的按钮时,只会响应一次。

11.8 侦听属性

下面请看一段代码并思考一个问题:如果我们希望 firstName 或 lastName 属性发生变化时,fullName 属性也能随之做出相应改变,那么应该如何实现呢?

<div id="app">
    <p>尊姓:{{firstName}}</p>
    <p>大名:{{lastName}}</p>
    尊姓:<input type="text" v-model="firstName" /><br/>
    大名:<input type="text" v-model="lastName" /><br/>
    <p>全名:{{fullName}}</p>
</div>

正确的做法是,此时需要对 firstName 或 lastName 属性进行侦听。具体来说,所谓侦听就是对属性进行监控,通过 watch 选项配置一个函数来监视某个响应式属性的变化。监视函数默认在数据变化时回调,且接收新值和旧值两个参数,其语法格式如下。

watch: {
    xxx(newVal, oldVal) {
    // 处理 xxx 数据变化后的逻辑
    }
}

对于上述案例,就是当 firstName 或 lastName 属性的值发生变化时,直接调用准备好的函数,对 fullName 属性做出相应修改。

在 watch 中声明对 firstName 和 lastName 属性进行侦听的函数。Vue 代码如下。

var app = new Vue({
    el: "#app",
    data: {
    firstName: "jim",
    lastName: "green",
    fullName: "jim green"
    },
    watch: {
    firstName: function (inputValue) {
    this jiullName = inputValue + " " + this. lastName;
    },
    lastName: function (inputValue) {
    this jiullName = this. firstName + " " + inputValue;
    }
    }
}); 

侦听前的初始页面如图 11-28 所示。

设置 watch 侦听后,修改 firstName 和 lastName 属性值,发现 fullName 属性值也随之改变,如图 11-29 所示。

f894ed9f1e8b78eda7aa87d22210c5acfc2c267b14871a5c81e36109baefb012.jpg

8bc6865fbdaea9936136444a4361de5970232e68ae7cdfc6ca144f0050836ed1.jpg

11.9 Vue 的生命周期

Vue 实例在应用过程中会有初始、挂载、更新,以及卸载的运行过程,我们将这个过程称为 Vue 实例的生命周期。对应在开发中,不同阶段程序会主动通过调用特定的回调函数来通知我们,我们将其称为 “生命周期钩子函数”。

11.9.1 生命周期流程图

Vue 实例的生命周期主要包括初始、挂载、更新、销毁几个不同的阶段,它们是按照顺序依次执行的。不是所有阶段的执行次数和目标都是相同的,其中初始、挂载和销毁这三个阶段都只执行一次;更新阶段是在不断发展和变化的,会不断地被触发。下面通过一个图来演示 Vue 实例的生命周期,如图 11-30 所示。

716f5e857b0cb7930140791d39cb6078df62a13a8f9a367dfcb728b97d17e61a.jpg

11.9.2 Vue 生命周期钩子函数

我们把一个对象从创建到被销毁的过程称为生命周期。而生命周期函数,就是在某个时刻会自动执行的函数。每个 Vue 实例在被创建时都要经过一系列的初始化过程:创建实例、装载模板、渲染模板等。Vue 为生命周期中的每个状态都设置了钩子函数(监听函数)。当 Vue 实例处于不同阶段的生命周期时,对应的函数就会被触发调用。Vue 中常见的八大生命周期钩子函数如表 11-1 所示。

表 11-1 Vue 中常见的八大生命周期钩子函数

钩子函数调用时间
beforeCreate()Vue 实例初始化前调用
created()Vue 实例初始化后调用
beforeMount()挂载到 DOM 树前调用
mounted()挂载到 DOM 树后调用
beforeUpdate()数据更新前调用
updated()数据更新后调用
beforeDestroy()Vue 实例销毁前调用
destroyed()Vue 实例销毁后调用

Vue 生命周期钩子函数的代码书写位置与 data、methods、watch 等配置属于同级并列关系。下面结合代码分析每个生命周期钩子函数并了解它们之间的差异。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
    <p ref="pRef">{{message}}</p> 
<button @click="message = '尚硅谷拥有最好的互联网技术培训'">更新内容</button>
</div>
<script>
    //创建Vue对象,挂载#app这个div标签
    var app = new Vue({
    el:"#app",
    data: {
    message: '欢迎来到尚硅谷'
    },
    //1.实例创建前
    beforeCreate() {
    console.log('beforeCreate', this.message, this.$refs.pRef)
    },
    //2.实例创建完成
    created() {
    console.log('created', this.message, this.$refs.pRef)
    },
    //3.数据挂载前
    beforeMount() {
    console.log('beforeMount', this.message, this.$refs.pRef)
    },
    //4.数据挂载完成
    mounted() {
    console.log('mounted', this.message, this.$refs.pRef)
    },
    //5.数据更新前
    beforeUpdate() {
    console.log('beforeUpdate')
    debugger; //设置 debugger 断点,进行调试,当 F12 打开的情况下,程序运行到此行会进入到 debug
模式
    },
    //6.数据更新完成
    updated() {
    console.log('updated')
    },
    //7.实例卸载前
    beforeDestroy() {
    console.log('beforeDestroy', this.$refs.pRef)
    debugger;
    },
    //8.实例卸载完成
    destroyed() {
    console.log('destroyed', this.$refs.pRef)
    }
    });
</script>
</body>
</html>

需要注意的是,ref 属性用于普通的 DOM 元素,表示引用指向的是该 DOM 元素,通过 $refs 可以获取该 DOM 的属性集合,作用与 JavaScript 的选择器类似。上述代码中,在<P>元素标签上使用 ref 属性,通过 “this.$refs.ref. 属性值” 可以获取整个 <p> 标签。

代码中演示了生命周期的 8 个钩子函数,下面分别进行分析。

- beforeCreate()

这是 Vue 实例的第一个生命周期钩子函数。在该钩子函数中出现了 this 对象,这个 this 对象指向的是当前 Vue 的实例对象。如果实例对象上有数据或者方法等内容,后续可以通过 this.xxx 的方式调用。值得一提的是,这个钩子函数是在 Vue 实例完成后立即触发的,但此时像 data 数据初始化、methods 方法调用,以及 watch 监控等内容都还没有进行初始化,更不用说 $el 的 DOM 元素对象了。因此在该生命周期钩子函数中,this.message 数据内容和 this.$refs.pRef 获取的 DOM 对象都为 undefined 未定义。

- created()

该钩子函数是在处理完所有和状态相关的选项后调用的,这就意味着调用钩子函数之前 data、methods、watch 等内容都已经设置完成。因此 this.message 内容会打印出 “欢迎来到尚硅谷” 字符串信息,但是由于挂载阶段还没有开始,所以 this.$refs.pRef 属性仍旧为 undefined。

- beforeMount()

这是在挂载 DOM 之前触发的生命周期钩子函数。如果有 template 模板内容,则会将其编译成 render 函数并调用。因为这一阶段还没有获取 DOM 对象,所以相关的内容都停留在虚拟 DOM 的阶段。因此在这一阶段,this.message 内容会打印出 “欢迎来到尚硅谷” 字符串信息,但 this.$refs.pRef 仍旧为 undefined。

- mounted()

在这一阶段的网页中,el 元素对象最终被实例的 $el 元素对象内容所代替,成功地将虚拟 DOM 内容渲染到了真实 DOM 对象上。用户现在可以查看到网页元素最终渲染的效果,因此当前 this.$refs.pRef 打印出来的结果是一个 DOM 元素渲染后的 <p> 标签。

到这里前面出现的生命周期钩子函数都只会按照讲解顺序执行一次。我们可以将其称为 “初始挂载阶段”,生命周期顺序以及相关内容输出打印的结果如图 11-31 所示。

ea40f4ad8f2edbe6683c6db86978c1ffa9d00951740708a3f15b48d8bd940988.jpg

- beforeUpdate()

由于组件的响应式状态变更,在即将更新 DOM 树前,会调用该生命周期钩子函数。比如示例代码中可以通过按钮修改 message 信息,这就产生了一个响应式数据的变更。在代码中的钩子函数中设置一个 debugger 断点进行效果的查看,最终会发现界面中显示的仍旧是数据修改之前的 DOM 内容,如图 11-32 所示。

- updated()

该生命周期钩子函数与 beforeUpdate 钩子函数类似,一个在更新 DOM 树前调用,一个在更新 DOM 树后调用。将之前的断点调试继续运行,则会发现界面中 message 内容已经被替换成了 “尚硅谷拥有最好的互联网技术培训” 字符串内容,如图 11-33 所示。

651d7614e0b4e56305aef8a6e11a1098aab21a4af853961971adac44fd483ff8.jpg

10f7202b771103a81b00c2bc09463ea0508787f1eb5026919ad70c12de4706de.jpg

- beforeDestroy()

当调用当前的生命周期钩子函数时,组件实例未被销毁,依然还保有全部的功能。值得注意的是,打开 F12 控制台,输入 app.$destroy()主动销毁 Vue 实例,该函数才能被调用。同样在该生命周期钩子函数中进行 debugger 断点调试,就可以查看 beforeDestroy 生命周期钩子函数的调用结果。此时界面中仍将保留 DOM 显示的状态,控制台中的 this.$refs.pRef 仍旧显示有元素对象,如图 11-34 所示。

42ecc9db33d09d8dfd63cb1370362a29106ff355c174f46710f10052e026319a.jpg

- destroyed()

与 beforeDestroy () 一样,需要在控制台输入 app.$destroy(),该函数才能被调用。该生命周期钩子函数在一个组件实例被卸载后调用,此时对应实例的 DOM 对象也不复存在。如图 11-35 所示,程序进入 destroyed 生命周期,显然 this.$refs.pRef 对象内容已经被清空,而网页中也不再显示任何的 DOM 元素。

图 11-35 网页中不显示任何的 DOM 元素

beforeCreate undefined undefineddemo01.html:23
created 欢迎来到尚硅谷 undefineddemo01.html:26
beforeMount 欢迎来到尚硅谷 undefineddemo01.html:29
mounted 欢迎来到尚硅谷 <p>欢迎来到尚硅谷</p>demo01.html:32
beforeUpdatedemo01.html:35
updateddemo01.html:39
> app.$destroy()
beforeDestroy <p>尚硅谷拥有最好的互联网技术培训</p>demo01.html:42
destroyed undefineddemo01.html:46

11.9.3 常用的生命周期方法

在这么多的生命周期钩子函数中,哪些才是项目开发中最为常用的呢?下面结合实际情况,从 6 个方面对各个生命周期钩子函数进行分析。

这里需要思考的是,在更新阶段是否要进行类似数据请求这样的操作,以及数据请求操作频率的问题,这将牵扯到性能相关的问题,因此这两个钩子函数的操作并不常用。如果一定要使用,还需要注意利用特

定的条件触发的频率限制。

(6)因为 destroyed () 已经完全卸载了实例对象,所以在这个钩子函数中操作的内容相对较少。如果需要进行定时器清除、取消监听、断开网络连接等操作,建议在 beforeDestroy 实例未完全卸载阶段进行,这样也能够确保 destroyed 阶段卸载的实例对象是比较干净纯粹的。

综上所述,mounted、beforeDestroy 等生命周期钩子函数是项目开发中最常用的钩子函数。

11.10 案例:水果库存静态页面功能优化

ca41e461aaa3489c8855bdcc1af384205bdac5ce78185ae0a993b435ee4ec638.jpg

下面通过 Vue 技术重新实现第 10 章 JavaScript 的案例。同样包括水果库存静态页面的功能:鼠标悬浮和离开时的操作、单价的更新,以及删除指定行。

在 section10 文件夹下,引入第 10 章案例的 CSS、HTML 文件和相关图片,以及 vue.js 文件。目录结构如图 11-36 所示。

其中,demo01.html 用于实现鼠标悬浮和离开时的操作、demo02.html 用于实现单价的更新、demo03.html 用于实现删除指定行。且这三个功能是递进式的,后一个功能在前一个功能的基础上实现。

11.10.1 鼠标悬浮效果实现

首先借助 Vue 来实现鼠标悬浮移动时不同效果的实现。

修改 demo01.html 文件,引入 vue.js 文件,添加 Vue 代码。示例代码如下。

<html>
<head>
    <title>鼠标滑动</title>
    <meta charset="utf-8"/>
    <link rel="stylesheet" href="css/demo01.css">
    <script src="js/vue.js"></script>
</head>
<body>
    <div id="div_container">
    <div id="div_fruit_list">
    <table id="tbl_fruit">
    <tr>
    <td>名称</td>
    <td>单价</td>
    <td>数量</td>
    <td>小计</td>
    <td>操作</td>
    </tr>
    <tr v-for="(fruit,index) in fruitList"
    @mouseover="changeBgColor(1)" @mouseout="changeBgColor(2)">
    <td>{{fruit.name}}</td>
    <td v-if="fruit.flag" class="numcss">{{fruit.num}}</td>
    <td v-else class="numcss">
    <input type="text" v-model="fruit.num" size="4">
    </td>
    <td>{{fruit.price}}</td>
    <td>{{fruit.num*fruit.price}}</td>
    <td><img src="imgs/del.jpg" class="delImg"></td>
    </tr>
    <tr> 
<td>总计</td>
<td colspan="4">{{sum}}</td>
</tr>
</table>
</div>
</div>
</body>
<script>
new Vue({
    el:"#div_container",
    data: {
    fruitList: [
    {name:"苹果",num:5,price:20,flag:true},
    {name:"香蕉",num:7,price:10,flag:true},
    {name:"梨儿",num:3,price:20,flag:true},
    {name:"西瓜",num:10,price:20,flag:true}
    ],
    sum:0
    },
    methods: {
    //1.鼠标滑动
    changeBgColor:function (i){
    if(i==1){
    event.target.parentElement.classList.add("hover");
    }else{
    event.target.parentElement.classList.remove("hover");
    }
    },
    //计算总计
    result:function () {
    var sum=0;
    for (let i = 0; i < this.fruitList.length; i++) {
    sum+=this.fruitList[i].num*this.fruitList[i].price;
    }
    this.sum=sum;
    }
    },
    //刷新总计
    created:function () {
    this.result();
    },
})
</script>
</html> 

在 demo01.css 文件中添加如下 CSS 样式,示例代码如下。

#tbl_fruit tr:hover {
    background-color: navy;
}
#tbl_fruit tr:hover td{
    color:white;
}
.numcss{
    width: 150px;
} 

运行代码,将鼠标悬浮在 “苹果” 所在行,如图 11-37 所示。

60fb1451b1b6e8e8026dd6dd3067863ec94e5c221862810463b1797181a46665.jpg

从图中可知,鼠标悬浮效果成功实现,鼠标停留在该行,该行整体颜色变深表示特指,与其他行区分开来;鼠标离开后,恢复原来的样式。

11.10.2 更新单价操作

本节内容对水果的单价进行优化,实现当鼠标单击单价列时,显示数字文本框,能够输入新的数额,更新单价信息。

修改 demo02.html 文件,添加 Vue 代码,示例代码如下。

<html>
<head>
    <title>更新单价</title>
    <meta charset="utf-8"/>
    <link rel="stylesheet" href="css/demo01.css">
    <script src="js/vue.js"></script>
</head>
<body>
    <div id="div_container">
    <div id="div_fruit_list">
    <table id="tbl_fruit">
    <tr>
    <td>名称</td>
    <td>单价</td>
    <td>数量</td>
    <td>小计</td>
    <td>操作</td>
    </tr>
    <tr v-for="(fruit,index) in fruitList"
    @mouseover="changeBgColor(1)" @mouseout="changeBgColor(2)">
    <td>{{fruit.name}}</td>
    <td @click="editNum(index)" v-if="fruit.flag"
    class="numcss">{{fruit.num}}</td>
    <td v-else class="numcss">
    <input type="number" v-model="fruit.num"
    @mouseout="updateNum(index)" size="4"
    @keydown="ckInput()">
    </td>
    <td>{{fruit.price}}</td>
    <td>{{fruit.num*fruit.price}}</td>
</body>
</html> 
<td><img src="imgs/del.jpg" class="delImg"></td>
</tr>

<tr>
    <td>总计</td>
    <td colspan="4">{{sum}}</td>
    </tr>
</table>
</div>
</div>
</body>
<script>
new Vue({
    el:"#div_container",
    data: {
    fruitList: [
    {name:"苹果",num:5,price:20,flag:true},
    {name:"香蕉",num:7,price:10,flag:true},
    {name:"梨儿",num:3,price:20,flag:true},
    {name:"西瓜",num:10,price:20,flag:true}
    ],
    sum:0
    },
    methods: {
    //2.更新单价
    //编辑单价
    editNum: function (index) {
    this.fruitList[index].flag = false;
    console.log(event.target.parentElement);
    event.target.parentElement.classList.remove("hover");
    },
    //更新单价
    updateNum: function (index) {
    //如果输入为空,值为0
    if(!event.target.value) {
    event.target.value = 0;
    }
    this.fruitList[index].num = event.target.value;
    this.fruitList[index].flag = true;
    this.result()
    },
    //校验输入的值是否为数字
    ckInput: function () {
    var kc = event.keyCode;
    console.log(kc);
    if (!(kc >= 48 && kc <= 57) || kc == 8 || kc == 13)) {
    event.returnValue = false;
    }
    if (kc == 13) {
    event.srcElement.blit();
    }
    }, 

//1. 鼠标滑动

changeBgColor: function (i) { 
if (i == 1) {
    event.target.parentElement.classList.add("hover");
} else {
    event.target.parentElement.classList.remove("hover");
},
//计算总计
result: function () {
var sum = 0;
for (let i = 0; i < this.fruitList.length; i++) {
sum += this.fruitList[i].num * this.fruitList[i].price;
}
this.sum = sum;
},
//刷新总计
created: function () {
this.result();
}
})
</script>
</html> 

运行代码,修改苹果的单价(元)为 10 如图 11-38 所示。

196244b3f96fd04249fdf6a49d8dca739767d718b2ac1ee2fcea74fb0602aaef.jpg

从图中可知,修改苹果单价(元)为 10 后,小计(元)为 200,总计(元)为 530,表明更新单价成功。并且保证输入时只有数字生效,如果输入字母则输入失败。

11.10.3 删除指定行

本节继续实现单击操作栏的删除图标,删除对应指定行的水果库存信息并更新总计。

修改 demo03.html 文件,添加 Vue 代码,示例代码如下。

<html>
<head>
    <title>删除水果</title>
    <meta charset="utf-8"/>
    <link rel="stylesheet" href="css/demo01.css">
    <script src="js/vue.js"></script>
</head>
<body>
    <div id="div_container"> 
<div id="div_fruit_list">
    <table id="tbl_fruit">
    <tr>
    <td>名称</td>
    <td>单价(元)</td>
    <td>数量(个)</td>
    <td>小计(元)</td>
    <td>操作</td>
    </tr>
    <tr v-for="(fruit,index) in fruitList"
    @mouseover="changeBgColor(1)" @mouseout="changeBgColor(2)">
    <td>{{fruit.name}}</td>
    <td @click="editNum(index)" v-if="fruit.flag"
    class="numcss">{{fruit.num}}</td>
    <td v-else class="numcss">
    <input type="number" v-model="fruit.num"
    @mouseout="updateNum(index)" size="4"
    @keydown="ckInput()">
    </td>
    <td>{{fruit.price}}</td>
    <td>{{fruit.num*fruit.price}}</td>
    <td><img src="imgs/del.jpg" class="delImg"
    @click="del(index)"></td>
    </tr>
    <tr>
    <td>总计</td>
    <td colspan="4">{{sum}}</td>
    </tr>
    </table>
    </div>
</div>
</body>
<script>
new Vue({
    el:"#div_container",
    data: {
    fruitList: [
    {name:"苹果",num:5,price:20,flag:true},
    {name:"香蕉",num:7,price:10,flag:true},
    {name:"梨儿",num:3,price:20,flag:true},
    {name:"西瓜",num:10,price:20,flag:true}
    ],
    sum:0
    },
    methods: {
    //3.删除水果信息
    del:function (index) {
    this.fruitList.splice(index,1);
    this.result()
    },
    //2.更新单价 
//编辑单价
editNum:function (index){
    this.fruitList[index].flag=false;
    console.log(event.target.parentElement);
    event.target.parentElement.classList.remove("hover");
},
//更新单价
updateNum:function (index){
    if(!event.target.value) {
    event.target.value = 0;
    }
    this.fruitList[index].num=event.target.value;
    this.fruitList[index].flag=true;
    this.result()
},
//校验输入的值是否为数字
ckInput:function () {
    var kc = event.keyCode;
    console.log(kc);
    if(!(kc>=48 && kc<=57) || kc==8 || kc==13)) {
    event.returnValue = false;
    }
    if(kc == 13) {
    event.srcElement.blur();
    }
},
//1.鼠标滑动
changeBgColor:function (i){
    if(i==1){
    event.target.parentElement.classList.add("hover");
    }else{
    event.target.parentElement.classList.remove("hover");
    }
},
//计算总计
result:function () {
    var sum=0;
    for (let i = 0; i < this.fruitList.length; i++) {
    sum+=this.fruitList[i].num*this.fruitList[i].price;
    }
    this.sum=sum;
},
//刷新总计
created:function () {
    this.result();
}
}) 

运行代码,单击苹果所在行的删除图标,如图 11-39 所示。

图 11-39 删除苹果信息

名称单价(元)数量(个)小计(元)操作
香蕉71070×
梨儿32060×
西瓜1020200×
总计(元)330

从图中可知,苹果信息删除成功,且总计随之改变。如果全部删除,如图 11-40 所示。从图中可知,所有水果信息删除成功,且总计为 0。

bed531fa78fd3a70f0455a033ff6d50f8e8686c650174e5fd9c79f94b91dfa1d.jpg

11.11 本章小结

本章主要介绍了 Vue 的相关知识,介绍了基于 HTML 的 Vue 模板语法,包括插值语法和指令语法两部分。详细介绍了声明式渲染、标签绑定、条件渲染、列表渲染等应用,以及 Vue 的事件处理和侦听属性 watch 的使用。另外还分析了 Vue 生命周期全过程,以及不同阶段钩子函数的调用情况。最后通过案例将之前 JavaScript 代码借助 Vue 实现,简化代码,进行优化,从而加深对 Vue 的理解和应用。通过本章的学习,希望大家可以灵活运用 Vue 进行前端页面开发。