0%

vue.js 实战

github.com

基础篇

基础篇将循序渐进地介绍 Vue.js 的核心功能,包括数据的双向绑定、计算属性、基本指令、自定义指令及组件等。通过对基础篇的学习,可以快速构建出 Vue.js 应用井直接用于生产环境。

初识 Vue.js

本章主要介绍与 Vue.js 有关的一些概念与技术, 并帮助你了解它们背后相关的工作原理。通过对本章的学习,即使从未接触过 Vue.js ,你也可以运用这些知识点快速构建出一个 Vue.js 应用。

Vue.js 是什么

Vue.js 的官方文档中是这样介绍它的。

简单小巧的核心,渐进式技术栈,足以应付任何规模的应用。

简单小巧是指Vue.js 压缩后大小仅有17 KB。所谓渐进式( Progressive ),就是你可以一步一步、有阶段性地来使用 Vue.js ,不必一开始就使用所有的东西。随着本书的不断介绍,你会深刻感受到这一点,这也正是开发者热爱 Vue.js 的主要原因之一。

使用 Vue.js 可以让 Web 开发变得简单, 同时也颠覆了传统前端开发模式。它提供了现代 Web 开发中常见的高级功能,比如:

  • 解耦视图与数据
  • 可复用的组件
  • 前端路由
  • 状态管理
  • 虚拟DOM ( Virtual DOM)

MVVM 模式

与知名前端框架 Angular、Ember 等一样, Vue.js 在设计上也使用 MVVM ( Model-View-View Mode l) 模式。

MVVM 模式是由经典的软件架构 MVC 衍生来的。当 View (视图层)变化时,会自动更新到 View Model (视图模型),反之亦然。View 和 View Model 之间通过双向绑定(data-binding )建立联系,如图1-1 所示。

image-20211220102201540

图1-1 MVVM 关系

Vue.js 有什么不同

如果你使用过 jQuery,那你一定对操作DOM 、绑定事件等这些原生 JavaScript 能力非常熟悉,比如我们在指定 DOM 中插入一个元素,并给它绑定一个点击事件:

1
2
3
4
5
6
7
if (showBtn) {
var btn = $('<button>Click me</button>');
btn.on('click', function() {
console.log('Clicked!');
})
$('#app').append(btn);
}

这段代码不难理解,操作的内容也不复杂,不过这样让我们的视图代码和业务逻辑紧藕合在一起,随着功能不断增加,直接操作 DOM 会使得代码越来越难以维护。

而 Vue.js 通过 MVVM 的模式拆分为视图与数据两部分,并将其分离。因此,你只需要关心你的数据即可, DOM 的事情 Vue 会帮你自动搞定,比如上面的示例用Vue.js 可以改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<div id="app">
<button v-if=” showBtn ” v-on:click=” handleClick ”>Click me </button>
</div>
</body>
<script>
new Vue ({
el: '#app',
data: {
showBtn: true
},
methods: {
handleClick: function() {
console.log('Clicked');
}
}
})
</script>

提示:暂时还不需要理解上述代码,这里只是快速展示Vue.js的写法,在后面的章节会详细介绍每个参数的用法。

如何可使用 Vue.js

每一个框架的产生都是为了解决某个具体的问题。在正式开始学习 Vue.js 前,我们先对传统前端开发模式和 Vue.js 的开发模式做一个对比,以此了解 Vue.js 产生的背景和核心思想。

传统的前端开发模式

前端技术在近几年发展迅速,如今的前端开发己不再是 10 年前写个 HTML 和 css 那样简单了,新的概念层出不穷,比如 ECMAScript 6 、Node.js 、NPM 、前端工程化等。这些新东西在不断优化我们的开发模式,改变我们的编程思想。

随着这些技术的普及, 一套可称为“万金油”的技术技被许多商业项目用于生产环境:

1
jQuery + RequireJS ( SeaJS ) + artTemplate ( doT ) + Gulp ( Grunt)

这套技术战以 jQuery 为核心,能兼容绝大部分浏览器,这是很多企业比较关心的,因为他们的客户很可能还在用IE 7 及以下浏览器。使用 RequireJS 或 SeaJS 进行模块化开发可以解决代码依赖混乱的问题, 同时便于维护及团队协作。使用轻量级的前端模板(如 doT )可以将数据与 HTML 模板分离。最后,使用自动化构建工具(如 Gulp )可以合并压缩代码,如果你喜欢写 Less 、Sass 以及现在流行的 ES 6 ,也可以帮你进行预编译。

这样一套看似完美无瑕的前端解决方案就构成了我们所说的传统前端开发模式,由于它的简单、高效、实用, 至今仍有不少开发者在使用。不过随着项目的扩大和时间的推移,出现了更复杂的业务场景,比如 SPA (单页面富应用〉、组件解耦等。为了提升开发效率,降低维护成本,传统的前端开发模式己不能完全满足我们的需求,这时就出现了如 Angular 、React 以及我们要介绍的主角 Vue.js 。

Vue.js 的开发模式

Vue.js 是一个渐进式的 JavaScript 框架,根据项目需求,你可以选择从不同的维度来使用它。

如果你只是想体验 Vue.js 带来的快感,或者开发几个简单的 HTML 5 页面或小应用,你可以直接通过 script 加载 CDN 文件,例如:

1
2
3
4
<!--自动识别最新稳定版本的Vue.js -->
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<!-- 指定某个具体版本的 Vue.js -->
<script src="https://unpkg.com/vue@2.1.6/dist/vue.min.js"></script>

两种版本都可以,如果你不太了解各版本的差别,建议直接使用最新的稳定版本。当然, 你也可以将代码下载下来, 通过自己的相对路径来引用。引入 Vue.js 框架后,在 body 底部使用 new Vue() 的方式创建一个实例, 这就是 Vue.js 最基本的开发模式。现在可以写入以下完整的代码来快速体验 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
26
27
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue 示例</title>
</head>
<body>
<div id="app">
<ul>
<li v-for="book in books">{{book.name}}</li>
</ul>
</div>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
books: [
{name: '《Vue.js 实战》'},
{name: '《JavaScript 语言精粹》'},
{name: '《JavaScript 高级程序设计》'},
]
}
})
</script>
</body>
</html>

在浏览器中访问它,会将图书列表循环显示出来,如图1-2 所示。

对于一些业务逻辑复杂, 对前端工程有要求的项目, 可以使用 Vue 单文件的形式配合 webpack 使用,必要时还会用到 vuex 来管理状态, vue-router 来管理路由。这里提到了很多概念, 目前还不必去过多了解,只是说明 Vue.js 框架的开发模式多样化, 后续章节会详细介绍,到时就会对整个 Vue 生态有所了解了。

image-20211220103603005

图1-2 Vue.js 示例在浏览器中访问的效果
了解了 Vue.js 的开发模式后,相信你已经迫不及待地想开启 Vue 的大门了。下一章,我们就直接进入话题,创建第一个 Vue 应用。

数据绑定和第一个 Vue 应用

学习任何一种框架,从一个 Hello World 应用开始是最快了解该框架特性的途径,我们先从一段简单的 HTML 代码开始,感受 Vue.js 最核心的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<! DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue 示例</title>
</head>
<body>
<div id="app">
<input type="text" v-model="name" placeholder="你的名字">
<h1>
你好,{{name}}
</h1>
</div>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
name: ''
}
})
</script>
</body>
</html>

这是一段简单到不能再简单的代码,但却展示出了 Vue.js 最核心的功能: 数据的双向绑定。在输入框输入的内容会实时展示在页丽的 h1 标签内,如图 2-1 所示。

image-20211220104944704

图 2-1 展示内容
提示:从本章开始,示例不再提供完整的代码,而是根据上下文,将 HTML 部分与JavaScript 部分单独展示,省咯了 `head `、`body `等标签以及 `Vue.js` 的加载等,读者可根据上例结构来组织代码.

Vue 实例与数据绑定

实例与数据

Vue.js 应用的创建很简单,通过构造函数Vue 就可以创建一个Vue 的根实例,并启动Vue 应用:

1
2
3
var app = new Vue( {
// 选项
})

变量 app 就代表了这个 Vue 实例。事实上,几乎所有的代码都是一个对象,写入Vue 实例的选项内的。

首先,必不可少的一个选项就是 el 。el 用于指定一个页面中己存在的 DOM 元素来挂载 Vue 实例,它可以是 HTML Element ,也可以是 css 选择器,比如:

1
2
3
4
5
6
<div id="app">

</div>
var app = new Vue({
el: docoument.getElementById('app') // 或者是 '#app'
})

挂载成功后,我们可以通过 app.$el 来访问该元素。Vue 提供了很多常用的实例属性与方法,都以 $ 开头,比如 $el , 后续还会介绍更多有用的方法。

回顾章节开始的 Hello World 代码,在 input 标签上,有一个 v-model 的指令,它的值对应于我们创建的 Vue 实例的 data 选项中的 name 字段, 这就是 Vue 的数据绑定。

通过 Vue 实例的 data 选项,可以声明应用内需要双向绑定的数据。建议所有会用到的数据都预先在data 内声明,这样不至于将数据散落在业务逻辑中,难以维护。

Vue 实例本身也代理了data 对象里的所有属性,所以可以这样访问:

1
2
3
4
5
6
7
var app = new Vue({
el: '#app',
data: {
a: 2
}
})
console.log(app.a); // 2

除了显式地声明数据外,也可以指向一个已有的变量,并且它们之间默认建立了双向绑定,当修改其中任意一个时,另一个也会一起变化:

1
2
3
4
5
6
7
8
9
10
11
var myData = {
a: 1
}
var app = new Vue({
el: '#app',
data: myData
})
console.log(app.a); // 1
// 修改属性,原数据也会随之修改
myData.a = 3;
console.log(app.a) // 3

生命周期

每个 Vue 实例创建时,都会经历一系列的初始化过程,同时也会调用相应的生命周期钩子,我们可以利用这些钩子,在合适的时机执行我们的业务逻辑。如果你使用过 jQuery , 一定知道它的 ready() 方法,比如以下示例:

1
2
3
$(document).ready(function() {
// DOM 加载完后,会执行这里的代码
});

Vue 的生命周期钩子与之类似,比较常用的有:

  • created 实例创建完成后调用,此阶段完成了数据的观测等,但尚未挂载, $el 还不可用。需要初始化处理一些数据时会比较有用,后面章节将有介绍
  • mounted el 挂载到实例上后调用,一般我们的第一个业务逻辑会在这里开始。
  • beforeDestroy 实例销毁之前调用。主要解绑一些使用 addEventListener 监听的事件等。

这些钩子与 el 和 data 类似,也是作为选项写入 Vue 实例内,并且钩子的 this 指向的是调用它的 Vue 实例:

1
2
3
4
5
6
7
8
9
10
11
12
var app = new Vue ({
el: '#app',
data: {
a: 2
},
created: function() {
console.log(this.a); // 2
},
mounted: function() {
console.log(this.$el) // <div id="app"></div>
}
})

插值与表达式

使用双大括号( Mustache 语法)是最基本的文本插值方法,它会自动将我们双向绑定的数据实时显示出来,例如:

1
2
3
4
5
6
7
8
9
10
11
<div id="app">
{{book}}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
book: '《Vue.js 实战》'
}
})
</script>

大括号里的内容会被替换为《Vue.js实战》,通过任何方法修改数据 book,大括号的内容都会被实时替换,比如下面的这个示例,实时显示当前的时间,每秒更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
{{data}}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
date: new Date()
},
mounted: function() {
var _this = this; // 声明一个变量指向Vue 实例this ,保证作用域一致
this.timer = setInterval(function(){
_this.date = new Date(); // 修改数据 date
}, 1000);
},
beforeDestory: function(){
if (this.timer) {
clearInterval(this.timer); // 在Vue 实例销毁前,清除我们的定时器
}
}
})
</script>

提示:这里的 1639966169000 输出的是浏览器默认的时间格式,比如 2017-01-02T14:04:49.470Z, 并非格式化的时间( 2017” 01-02 22:04:49 ),所以需要注意时区.有多种方法可以对时间格式化,比如赋值前先使用自定义的函数处理。Vue 的过滤器( filter )或计算属性(computed )也可以实现,稍后会介绍到。

如果有的时候就是想输出 HTML,而不是将数据解释后的纯文本,可以使用 v-html:

1
2
3
4
5
6
7
8
9
10
11
<div id="app">
<span v-html="link"></span>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
link: '<a href="#">这是一个连接</a>'
}
})
</script>

link 的内容将会被渲染为一个具有点击功能的 a 标签,而不是纯文本。这里要注意,如果将用户产生的内容使用 v-html 输出后,有可能导致 xss 攻击,所以要在服务端对用户提交的内容进行处理, 一般可将尖括号 “<>” 转义。

如果想显示 {{ }} 标签,而不进行替换, 使用 v-pre 即可跳过这个元素和它的子元素的编译过程,例如:

1
<span v-pre>{{这里的内容是不会被编译的}}</span>

在 {{ }} 中,除了简单的绑定属性值外,还可以使用 JavaScript 表达式进行简单的运算、三元运算等,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13

<div id="app">
{{number / 10}}
{{isOK ? '确定' : '取消'}}
{{testsplit(',').reverse().join(',')}}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
number: 100,
isOK: false,
text: '123,456'

显示结果依次为: 10 、取消、456 , 123 。

Vue.js 只支持单个表达式,不支持语句和流控制。另外, 在表达式中,不能使用用户自定义的全局变量, 只能使用 Vue 白名单内的全局变量, 例如 Math 和 Date 。以下是一些无效的示例:

1
2
3
4
<!-- 这是语旬,不是表达式 -->
{{var book ='Vue.js 实战'}}
<!--不能使用流控制,要使用三元运算 -->
{{if (ok) return msg}}

过滤器

Vue.js 支持在 {{ }} 插值的尾部添加一小管道符 “(|)” 对数据进行过滤,经常用于格式化文本,比如字母全部大写、货币千位使用逗号分隔等。过滤的规则是自定义的, 通过给 Vue 实例添加选项 filters 来设置, 例如在上一节中实时显示当前时间的示例,可以对时间进行格式化处理:

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
<div id="app">
{{date | formateDate}}
</div>
<script>
// 在月份、日期、小时等小于10 时前面补0
var padDate = function(value) {
return value <10 ? '0' + value : value;
};
var app = new Vue({
el: '#app',
data: {
date: new Date()
},
filters: {
formateDate: function(value) { //这里的value 就是需要过滤的数据
var date = new Date(value);
var year = date.getFullYear();
var month = padDate(date.getMonth() + 1);
var day = padDate(date.getDate());
var hours = padDate(date.getHours());
var minutes = padDate(date.getMinutes());
var seconds = padDate(date.getSeconds());
// 将整理好的数据返回出去
return year + '-' + month + '-' + day + '-' + hours + ':' + minutes + ':' + seconds;
}
},
mounted: function() {
var _this = this; // 声明一个变量指向 Vue 实例 this,保证作用域一致
this.timer = setInterval(function() {
_this.date = new Date(); // 修改数据 date
}, 1000);
},
beforeDestory: function() {
if (this.timer) {
clearInterval(this.timer); // 在 Vue 实例销毁前, 清除我们的定时器
}
}
})

</script>

过滤器也可以串联,而且可以接收参数,例如:

1
2
3
4
<!--串联-->
{{message | filterA | filterB}}
<!--接收参数-->
{{message | filterA('arg1', 'arg2')}}

这里的字符串 arg1 和 arg2 将分别传给过滤器的第二个和第三个参数,因为第一个是数据本身。

提示:过滤器应当用于处理简单的文本转换,如果要实现更为复杂的数据变换,应该使用计算属性,下一章中会详细介绍它的用法。

指令与事件

指令( Directives )是 Vue.js 模板中最常用的一项功能,它带有前缀 v-, 在前文我们已经使用过不少指令了,比如 v-if、v-html 、v-pre 等。指令的主要职责就是当其表达式的值改变时,相应地将某些行为应用到DOM 上,以v-if 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<p v-if="show">
显示这段文本
</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
show: true
}
})
</script>

当数据 show 的值为 true 时, p 元素会被插入,为 false 时则会被移除。数据驱动 DOM 是 Vue.js 的核心理念,所以不到万不得已时不要主动操作DOM ,你只需要维护好数据, DOM 的事 Vue 会帮你优雅的处理。

Vue.js 内置了很多指令,帮助我们快速完成常见的DOM 操作,比如循环渲染、显示与隐藏等。在第5 章会详细地介绍这些内置指令,但在此之前,你需要先知道v-bind 和 v-on 。

v-bind 的基本用途是动态更新 HTML 元素上的属性,比如 id、class 等,例如下面几个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<a v-bind:href="url">链接</a>
<img v-bind:src="imgUrl">
</div>
<script>
var app = new Vue({
el: '#app',
data: {
url: 'https://www.github.com',
imgUrl: 'http://xxx.xxx.xx/img.png'
}
})
</script>

示例中的链接地址与图片的地址都与数据进行了绑定,当通过各种方式改变数据时,链接和图片都会自动更新。上述示例渲染后的结果为:

1
2
<a href ="https://www.github.com">链接</a>
<img src="http://xxx.xxx . xx/img.png">

以上是介绍 v-bind 最基本的用法,它在 Vue.js 组件中还有着重要的作用,将在第 4 章和第 7 章中详细介绍。

另一个非常重要的指令就是 v-on,它用来绑定事件监听器,这样我们就可以做一些交互了,先来看下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<p v-if="show">
这是一段文本
</p>
<button v-on:click="handleClose">
点击隐藏
</button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
show: true
},
methods: {
handleClose: function() {
this.show = false;
}
}
})
</script>

在 button 按钮上,使用 v-on:click 给该元素绑定了一个点击事件,在普通元素上, v-on 可以监听原生的 DOM 事件,除了 click 外,还有 dblclick、keyup, mousemove 等。表达式可以是一个方法名,这些方法都写在 Vue 实例的 methods 属性内,并且是函数的形式,函数内的 this 指向的是当前Vue 实例本身,因此可以直接使用 this.xxx 的形式来访问或修改数据,如示例中的 this.show = false 把数据 show 修改为了 false,所以点击按钮时,文本 p 元素就被移除了。

表达式除了方法名, 也可以直接是一个内联语旬,上例也可以改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
<p v-if="show">
这是一段文本
</p>
<button v-on:click="show = false">
点击隐藏
</button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
show: true
}
})
</script>

如果绑定的事件要处理复杂的业务逻辑,建议还是在 methods 里声明一个方法,这样可读性更强也好维护。

Vue.js 将 methods 里的方法也代理了,所以也可以像访问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
<div id="app">
<p v-if="show">
这是一段文本
</p>
<button v-on:click="handleClose">
点击隐藏
</button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
show: true
},
methods: {
handleClose: function() {
this.close();
},
close: function() {
this.show = false;
}
}
})
</script>

在handleClose 方法内,直接通过 this.close () 调用了 close() 函数。在上面示例中是多此一举的,只是用于演示它的用法,在业务中会经常用到,例如以下几种用法都是正确的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
var app = new Vue({
el: '#app',
data: {
show: true
},
methods: {
init: function(text){
console.log(text); }
},
mounted: function() {
this.init('在初始化时调用'); // 在初始化时调用
}
})
app.init('通过外部调用'); // 在 Vue 实例外部调用
</script>

更多关于v-on 事件的用法将会在第 7 章中详细介绍。

语法糖

语法糖是指在不影响功能的情况下, 添加某种方法实现同样的效果, 从而方便程序开发。

Vue.js 的 v-bind 和 v-on 指令都提供了语法糖, 也可以说是缩写, 比如 v-bind , 可以省略 v-bind, 直接写一个冒号 “:”

1
2
3
4
5
<a v-bind:href="url">链接</a>
<img v-bind:src="imgUrl">
<!-- 缩写为 -->
<a :href="url">链接</a>
<img :src="imgUrl">

v-on 可以直接用 “@” 来缩写:

1
2
3
4
5
6
7
<button v-on:click="handleClose">
点击隐藏
</button>
<!-- 缩写为 -->
<button @click="handleClose">
点击隐藏
</button>

使用语法糖可以简化代码的书写, 从下一章开始, 所有示例的 v-bind 和 v-on 指令将默认使用语法糖的写法。

计算属性

模板内的表达式常用于简单的运算,当其过长或逻辑复杂时,会难以维护,本章的计算属性就是用于解决该问题的。

什么是计算属性

通过上一章的介绍,我们己经可以搭建出一个简单的 Vue 应用,在模板中双向绑定一些数据或表达式了。但是表达式如果过长,或逻辑更为复杂时,就会变得臃肿甚至难以阅读和维护,比如:

1
2
3
<div>
{{text.split(',').reverse().join(',')}}
</div>

这里的表达式包含 3 个操作,并不是很清晰,所以在遇到复杂的逻辑时应该使用计算属性。上例可以用计算属性进行改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
{{reversedText}}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
text: '123,456'
},
computed: {
reversedText: function() {
// 这里的this指向的时当前的Vue实例
return this.text.split(',').reverse().join(',');
}
}
})
</script>

所有的计算属性都以函数的形式写在 Vue 实例内的 computed 选项内,最终返回计算后的结果。

计算属性用法

在一个计算属性里可以完成各种复杂的逻辑,包括运算、函数调用等,只要最终返回一个结果就可以。除了上例简单的用法,计算属性还可以依赖多个 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<div id="app">
总价: {{prices}}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
package1: [
{
name: 'iphone 7',
price: 7199,
count: 2
},
{
name: 'iPad',
price: 2888,
count: 1
}
],
package2: [
{
name: 'apple',
price: 3,
count: 5
},
{
name: 'banana',
price: 2,
count: 10
}
]
},
computed: {
prices: function() {
var prices = 0;
for (var i = 0; i < this.package1.length; i++) {
prices += this.package1[i].price * this.package1[i].count;
}
for (var i = 0; i < this.package2.length; i++) {
prices += this.package2[i].price * this.package2[i].count;
}
return prices;
}
}
})
</script>

当 packagel 或 package2 中的商品有任何变化,比如购买数量变化或增删商品时,计算属性 prices 就会自动更新, 视图中的总价也会自动变化。

每一个计算属性都包含一个 getter 和一个 setter ,我们上面的两个示例都是计算属性的默认用法, 只是利用了 getter 来读取。在你需要时,也可以提供一个 setter 函数, 当手动修改计算属性的值就像修改一个普通数据那样时,就会触发 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
<div id="app">
姓名:{{fullName}}
</div>>
<script>
var app = new Vue({
el: '#app',
data: {
firstName: 'Jack',
lastName: 'Green'
},
computed: {
fullName: {
// getter, 用于读取
get: function() {
return this.firstName + ' ' + this.lastName;
},
// setter, 写入时出发
set: function(newValue) {
var names = newValue.split(' ');
this.firstName = names[0];
this.lastName = names[names.length - 1];
}
}
}
})
</script>

当执行 app.fullName ='John Doe';时,setter 就会被调用,数据 firstName 和 lastName 都会相对更新,视图同样也会更新。

绝大多数情况下,我们只会用默认的 getter 方法来读取一个计算属性,在业务中很少用到 setter, 所以在声明一个计算属性时,可以直接使用默认的写法,不必将getter 和 setter 都声明。

计算属性除了上述简单的文本插值外,还经常用于动态地设置元素的样式名称 class 和内联样式 style ,在下章会介绍这方面的内容。当使用组件时,计算属性也经常用来动态传递 props ,这也会在第7 章组件里详细介绍。

计算属性还有两个很实用的小技巧容易被忽略:一是计算属性可以依赖其他计算属性; 二是计算属性不仅可以依赖当前 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
<div id="app1">

</div>
<div id="app2">
{{reversedText}}
</div>
<script>
var app1 = new Vue({
el: '#app1',
data: {
text: '123,456'
}
});

var app2 = new Vue({
el: '#app2',
computed: {
reversedText: function() {
// 这里依赖的是实例 app1 的数据 text
return app1.text.split(',').reverse().join(',');
}
}
})
</script>

这里我们创建了两个 Vue 实例 app1 和 app2 , 在 app2 的计算属性 reversedText 中,依赖的是 app1 的数据text ,所以当 text 变化时,实例 app2 的计算属性也会变化。这样的用法在后面章节介绍的组件和组件化里会用到,尤其是在多人协同开发时很常用,因为你写的一个组件所用得到的数据需要依赖他人的组件提供。随着后面对组件的深入会慢慢意识到这点, 现在可以不用太过了解。

计算属性缓存

在上一章介绍指令与事件时,你可能发现调用 methods 里的方法也可以与计算属性起到同样的作用,比如本章第一个示例可以用 methods 改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<!-- 注意,这里的 reversedText 是方法,所以要带() -->
{{reversedText()}}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
text: '123,456'
},
methods: {
reversedText: function() {
// 这里的 this 指向的是当前 Vue 实例
return this.text.split(',').reverse().join(',');
}
}
})
</script>

没有使用计算属性,在 methods 里定义了一个方法实现了相同的效果,甚至该方法还可以接受参数,使用起来更灵活。既然使用 methods 就可以实现,那么为什么还需要计算属性呢?原因就是计算属性是基于它的依赖缓存的。一个计算属性所依赖的数据发生变化时,它才会重新取值,所以 text 只要不改变,计算属性也就不更新,例如:

1
2
3
4
5
computed: {
now: functon() {
return Date.now();
}
}

这里的 Date.now() 不是响应式依赖,所以计算属性 now 不会更新。但是 methods 则不同,只要重新渲染,它就会被调用,因此函数也会被执行。

使用计算属性还是methods 取决于你是否需要缓存,当遍历大数组和做大量计算时,应当使用计算属性,除非你不希望得到缓存。

v-bind 及class 与style 绑定

DOM 元素经常会动态地绑定一些class 类名或style 样式,本章将介绍使用v-bind 指令来绑定class 和style 的多种方法。

了解v-bind 指令

在第2 章时,我们已经介绍了指令v-bind 的基本用法以及它的语法糖,它的主要用法是动态更新 HTML元素上的属性,回顾一下下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
<a v-bind:href="url">链接</a>
<img v-bind:src="imgUrl">
<!-- 缩写为 -->
<a :href="url">链接</a>
<img :src="imgUrl">
</div>
<script>
var app = new Vue({
el: '#app',
data: {
url: 'https://www.github.com',
imgUrl: 'http://xxx.xxx.xx/img.png'
}
})
</script>

链接的href 属性和图片的src 属性都被动态设置了,当数据变化时,就会重新渲染。

在数据绑定中,最常见的两个需求就是元素的样式名称class 和内联样式 style 的动态绑定,它们也是HTML 的属性,因此可以使用v-bind 指令。我们只需要用v-bind 计算出表达式最终的字符串就可以,不过有时候表达式的逻辑较复杂,使用字符串拼接方法较难阅读和维护,所以Vue.js 增强了对class 和style 的绑定。

绑定class 的几种方式

对象语法

给 v-bind:class 设置一个对象,可以动态地切换 class ,例如:

1
2
3
4
5
6
7
8
9
10
11
<div id="app">
<div :class="{'active': isActive}"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
isActive: true
}
})
</script>

上面示例中,类名 active 依赖于数据 isActive ,当其为 true 时, div 会拥有类名 Active ,为 false 时则没有,所以上例最终渲染完的结果是:

1
<div class="active"></div>

对象中也可以传入多个属性,来动态切换 class 。另外,:class 可以与普通 class 共存,例如:

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<div class="static" :class="{'active': isActive, 'error': isError}"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
isActive: true,
isError: false
}
})
</script>

:class 内的表达式每项为真时,对应的类名就会加载, 上面渲染后的结果为:

1
<div class="static active"></div>

当数据 isActive 或 isError 变化时,对应的 class 类名也会更新。比如当 isError 为 true 时,渲染后的结果为:

1
<div class="static active error"></div>

当 :class 的表达式过长或逻辑复杂时,还可以绑定一个计算属性,这是一种很友好和常见的用法,一般当条件多于两个时,都可以使用 data 或 computed ,例如使用计算属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<div :class="classes"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
isActive: true,
error: null
},
computed: {
classes: function() {
return {
active: this.isActive && !this.error,
'text-fail': this.error && this.error.type === 'fail'
}
}
}
})
</script>

除了计算属性,你也可以直接绑定一个0均ect 类型的数据,或者使用类似计算属性的methods

数组语法

当需要应用多个 class 时, 可以使用数组语法, 给 :class 绑定一个数组,应用一个 class 列表:

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<div :class="{activeCls, errorCls}"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
activeCls: 'active',
errorCls: 'error'
}
})
</script>

渲染后结果为:

1
<div class="active error"></div>

也可以使用三元表达式来根据条件切换 class , 例如下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<div :class="{isActive ? activeCls : '', errorCls}"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
isActive: true,
activeCls: 'active',
errorCls: 'error'
}
})
</script>

样式 error 会始终应用,当数据 isActive 为真时,样式 active 才会被应用。class 有多个条件时,这样写较为烦琐,可以在数组语法中使用对象语法:

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<div :class="[{'active': isActive}, errorCls]"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
isActive: true,
errorCls: 'error'
}
})
</script>

当然,与对象语法一样,也可以使用 data 、computed 和 methods 三种方法,以计算属性为例:

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">
<button :class="classes"></button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
size: 'large',
disabled: true
},
computed: {
classes: function() {
return [
'btn',
{
['btn-' + this.size]: this.size != '',
['btn-disabled']: this.disabled
}
];
}
}
})
</script>

示例中的样式 btn 会始终应用,当数据 size 不为空时,会应用样式前缀 btn-, 后加 size 的值; 当数据 disabled 为真时,会应用样式 btn-disabled ,所以该示例最终渲染的结果为:

1
<button class="btn btn-large btn-disabled"></button>

使用计算属性给元素动态设置类名,在业务中经常用到,尤其是在写复用的组件时,所以在开发过程中,如果表达式较长或逻辑复杂,应该尽可能地优先使用计算属性。

在组件上使用

如果直接在自定义组件上使用class 或 :class , 样式规则会直接应用到这个组件的根元素上,例如声明一个简单的组件:

1
2
3
Vue.component('my-component', {
template: '<p class="article">一些文本</p>'
})

然后在调用这个组件时,应用上面两节介绍的对象语法或数组语法给组件绑定class ,以对象语法为例:

1
2
3
4
5
6
7
8
9
10
11
<div id="app">
<my-component :class="{'active': isActive}"></my-component>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
isActive: true
}
})
</script>

最终组件渲染后的结果为:

1
<p class="article">一些文本</p>

这种用法仅适用于自定义组件的最外层是一个根元素,否则会无效,当不满足这种条件或需要给具体的子元素设置类名时,应当使用组件的props 来传递。这些用法同样适用于下一节中绑定内联样式style 的内容。

绑定内联样式

使用 v-bind:style (即 :style ) 可以给元素绑定内联样式,方法与 :class 类似,也有对象语法和数组语法,看起来很像直接在元素上写 CSS:

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<div :style="{'color': color, 'fontSize': fontSize + 'px'}">文本</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
color: 'red',
fontSize: 14
}
})
</script>

css 属性名称使用驼峰命名( came!Case )或短横分隔命名( kebab-case ), 擅染后的结果为:

1
<div style="color: red; font-size: 14px">文本</div>

大多数情况下, 直接写一长串的样式不便于阅读和维护,所以一般写在 data 或 computed 里,以 data 为例改写上面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
<div :style="sytles">
文本
</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
styles: {
color: 'red',
fontSize: 14 + 'px'
}
}
})
</script>

应用多个样式对象时, 可以使用数组语法:

1
<div style="color: red; font-size: 14px">文本</div>

在实际业务中,:style 的数组语法并不常用, 因为往往可以写在一个对象里面: 而较为常用的应当是计算属性。

另外, 使用 :style 时, Vue .js 会自动给特殊的 css 属性名称增加前缀, 比如 transform 。

内置指令

回顾一下第 2.2 节,我们己经介绍过指令( Directive )的概念了, Vue.js 的指令是带有特殊前缀 “ v-” 的 HTML 特性, 它绑定一个表达式,并将一些特性应用到 DOM 上。其实我们已经用到过很多 Vue 内置的指令,比如 v-html 、v-pre ,还有上一章的 v-bind 。本章将继续介绍 Vue.js 中更多常用的内置指令。

基本指令

v-cloak

v-cloak 不需要表达式,它会在Vue 实例结束编译时从绑定的 HTML 元素上移除, 经常和 css 的 display: none;配合使用:

1
2
3
4
5
6
7
8
9
10
11
<div id="app" v-cloak>
{{message}}
</div>
<script>
var app = new Vue ({
el: '#app',
data: {
message: '这是一段文本'
}
})
</script>

这时虽然己经加了指令 v-cloak ,但其实并没有起到任何作用,当网速较慢,Vue.js 文件还没加载完时,在页面上会显示 {{message}} 的字样,直到 Vue 创建实例、编译模板时, DOM 才会被替换,所以这个过程屏幕是有闪动的。只要加一句 css 就可以解决这个问题了:

1
2
3
[v-cloak] {
display: none;
}

在一般情况下, v-cloak 是一个解决初始化慢导致页面闪动的最佳实践,对于简单的项目很实用,但是在具有工程化的项目里,比如后面进阶篇将介绍 webpack 和 vue-router 时,项目的HTML结构只有一个空的 div 元素,剩余的内容都是由路由去挂载不同组件完成的,所以不再需要 v-cloak。

v-once

v-once 也是一个不需要表达式的指令,作用是定义它的元素或组件只渲染一次,包括元素或组件的所有子节点。首次渲染后,不再随数据的变化重新渲染,将被视为静态内容,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<span v-once>{{message}}</span>
<div v-once>
<span>{{message}}</span>
</div>
</div>
<script>
var app = new Vue ({
el: '#app',
data: {
message: '这是一段文本'
}
})
</script>

v-once 在业务中也很少使用,当你需要进一步优化性能时,可能会用到。

条件渲染指令

v-if, v-else-if, v-else

与 JavaScript 的条件语句 if、else 、else if 类似, Vue.js 的条件指令可以根据表达式的值在 DOM 中渲染或销毁元素/组件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<p v-if="status === 1">当status为1时显示该行</p>
<p v-else-if="status === 2">当status为2时显示该行</p>
<p v-else>否则显示该行</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
status: 1
}
})
</script>

v-else-if 要紧跟 v-if, v-else 要紧跟 v-else-if 或v-if,表达式的值为真时, 当前元素/组件及所有子节点将被渲染,为假时被移除。如果一次判断的是多个元素,可以在 Vue.js 内置的<template> 元素上使用条件指令,最终渲染的结果不会包含该元素,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
<template v-if="status === 1">
<p>这是一段文本</p>
<p>这是一段文本</p>
<p>这是一段文本</p>
</template>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
status: 1
}
})
</script>

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
<div id="app">
<template v-if="type === 'name'">
<label>用户名:</label>
<input placeholder="输入用户名">
</template>
<template v-else>
<label>邮箱:</label>
<input placeholder="输入邮箱">
</template>
<button @click="handleToggleClick">切换输入类型</button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
type: 'name'
},
methods: {
handleToggleClick: function() {
this.type = this.type === 'name' ? 'mail' : 'name';
}
}
})
</script>

如图 5-1 和图 5-2 所示,键入内容后,点击切换按钮,虽然 DOM 变了,但是之前在输入框键入的内容并没有改变,只是替换了 placeholder 的内容,说明 <input>元素被复用了。

image-20220207113123562

图5-1 切换前的状态

image-20220207113155721

图5-2 切换后的状态

如果你不希望这样做,可以使用 Vue.js 提供的 key 属性,它可以让你自己决定是否要复用元素, 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
<div id="app">
<template v-if="type === 'name'">
<label>用户名:</label>
<input placeholder="输入用户名" key="name-input">
</template>
<template v-else>
<label>邮箱:</label>
<input placeholder="输入邮箱" key="mail-input">
</template>
<button @click="handleToggleClick">切换输入类型</button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
type: 'name'
},
methods: {
handleToggleClick: function() {
this.type = this.type === 'name' ? 'mail' : 'name';
}
}
})
</script>

给两个 <input> 元素都增加 key 后, 就不会复用了,切换类型时键入的内容也会被删除,不过<label>元素仍然是被复用的,因为没有添加 key 属性。

v-show

v-show 的用法与 v-if 基本一致,只不过 v -show 是改变元素的 css 属性 display 。当 v-show 表达式的值为 false 时, 元素会隐藏,查看 DOM 结构会看到元素上加载了内联样式 display : none;, 例如:

1
2
3
4
5
6
7
8
9
10
11
<div id="app">
<p v-show="status === 1">当status为1时显示该行</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
status: 2
}
})
</script>

渲染后的结果为:

1
<p style="display: none;">当status为1时显示该行</p>

提示: v-show 不能在 <template> 上使用。

v-if 与v-show 的选择

v-if 和 v-show 具有类似的功能,不过 v-if 才是真正的条件渲染,它会根据表达式适当地销毁或重建元素及绑定的事件或子组件。若表达式初始值为 false ,则一开始元素/组件并不会渲染,只有当条件第一次变为真时才开始编译。

而v-show 只是简单的 css 属性切换,无论条件真与否,都会被编译。相比之下, v-if 更适合条件不经常改变的场景,因为它切换开销相对较大,而 v-show 适用于频繁切换条件。

列表渲染指令v-for

基本用法

当需要将一个数组遍历或枚举一个对象循环显示时,就会用到列表渲染指令 v-for 。它的表达式需结合 in 来使用,类似 item in items 的形式,看下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<ul>
<li v-for="book in books">{{book.name}}</li>
</ul>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
books: [
{name: '《Vue.js 实战》'},
{name: '《Vue.js 实战》'},
{name: '《Vue.js 实战》'},
]
}
})
</script>

我们定义一个数组类型的数据 books , 用 v-for 将 <li> 标签循环渲染, 效果如图 5 -3 所示。

image-20220207153656216

图 5-3 列表循环结果
在表达式中, books 是数据, book 是当前数组元素的别名, 循环出的每个 `
  • `内的元素都可以访问到对应的当前数据 book 。列表渲染也支持用 of 来代替 in作为分隔符,它更接近 JavaScript 法代器的语法:
    1
    <li v-for="book of books">{{book.name}}</li>

    v-for 的表达式支持一个可选参数作为当前项的索引, 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <div id="app">
    <ul>
    <li v-for="(book, index) in books">{{index}} - {{book.name}}</li>
    </ul>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    books: [
    {name: '《Vue.js 实战》'},
    {name: '《Vue.js 实战》'},
    {name: '《Vue.js 实战》'},
    ]
    }
    })
    </script>

    分隔符 in 前的语句使用括号, 第二项就是 books 当前项的索引,渲染后的结果如图 5-4 所示。

    image-20220207153948237

    图5-4 含有index 逃项的列表渲染结果

    提示: 如果你使用过Vue.js 1.x 的版本,这里的 index 也可以由内直的 $index 代替, 不过在 2.x 里取消了该用法。

    与 v-if 一样, v-for 也可以用在内置标签<template>上, 将多个元素进行渲染:

    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
    <div id="app">
    <ul>
    <template v-for="book in books">
    <li>书名:{{book.name}}</li>
    <li>作者:{{book.author}}</li>
    </template>
    </ul>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    books: [
    {
    name: '《Vue.js 实战》',
    author: '梁灏'
    },
    {
    name: '《Vue.js 实战》'
    author: 'Douglas Crockford'
    },
    {
    name: '《Vue.js 实战》',
    author: 'Nicholas C.Zakas'
    },
    ]
    }
    })
    </script>

    除了数组外, 对象的属性也是可以遍历的,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <div id="app">
    <span v-for="value in user">{{value}}</span>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    user: {
    name: 'Aresn',
    gender: '男',
    age: 26
    }
    }
    })
    </script>

    渲染后的结果为:

    1
    <span>Aresn</span><span></span><span>26</span>

    遍历对象属性时,有两个可选参数,分别是键名和索引:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <div id="app">
    <ul>
    <li v-for="(value, key, index) in user">{{index}} - {{key}}: {{value}}</li>
    </ul>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    user: {
    name: 'Aresn',
    gender: '男',
    age: 26
    }
    }
    })
    </script>

    渲染后的结果如图 5-5 所示。

    image-20220207160508314

    图5-5 遍历对象的渲染结果

    v-for 还可以迭代整数:

    1
    2
    3
    4
    5
    6
    7
    8
    <div id="app">
    <span v-for="n in 10">{{n}}</span>
    </div>
    <script>
    var app = new Vue({
    el: '#app'
    })
    </script>

    渲染后的结果为:
    1 2 3 4 5 6 7 8 9 10

    数组更新

    Vue 的核心是数据与视图的双向绑定,当我们修改数组时, Vue 会检测到数据变化,所以用 v-for 渲染的视图也会立即更新。Vue 包含了一组观察数组变异的方法,使用它们改变数组也会触发视图更新:

    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()

    例如,我们将之前一个示例的数据 books 添加一项:

    1
    2
    3
    4
    app.books.push({
    name: '《CSS揭秘》',
    author: '[希] Lea Verou'
    });

    使用以上方法会改变被这些方法调用的原始数组,有些方法不会改变原数组,例如:

    • filter()

    • concat()

    • slice()

    它们返回的是一个新数组,在使用这些非变异方法时,可以用新数组来替换原数组,还是之前展示书目的示例,我们找出含有 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
    <div id="app">
    <ul>
    <template v-for="book in books">
    <li>书名: {{book.name}}</li>
    <li>作者: {{book.author}}</li>
    </template>
    </ul>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    books: [
    {
    name: '《Vue.js 实战》',
    author: '梁灏'
    },
    {
    name: '《Vue.js 实战》'
    author: 'Douglas Crockford'
    },
    {
    name: '《Vue.js 实战》',
    author: 'Nicholas C.Zakas'
    },
    ]
    }
    });
    app.books = app.books.filter(function(item){
    return item.name.match(/JavaScript/);
    })
    </script>

    渲染的结果中,第一项《Vue. 实战》被过滤掉了,只显示了书名中含有 JavaScript 的选项。

    Vue 在检测到数组变化时,并不是直接重新渲染整个列表,而是最大化地复用 DOM 元素。替换的数组中,含有相同元素的项不会被重新渲染,因此可以大胆地用新数组来替换旧数组,不用担心性能问题。

    需要注意的是,以下变动的数组中, Vue 是不能检测到的,也不会触发视图更新:

    • 通过索引直接设置项,比如 app.books[3] = { … }.
    • 修改数组长度,比如 app.books.length = 1 .

    解决第一个问题可以用两种方法实现同样的效果,第一种是使用 Vue 内置的set 方法:

    1
    2
    3
    4
    Vue.set(app.books, 3, {
    name: '《CSS揭秘》',
    author: '[希] Lea Verou'
    })

    如果是在 webpack 中使用组件化的方式(进阶篇中将介绍〉,默认是没有导入Vue 的,这时可以使用 $set ,例如:

    1
    2
    3
    4
    this.$set(app.books, 3, {
    name: '《CSS揭秘》',
    author: '[希] Lea Verou'
    }) //这里的 this 指向的就是当前组件实例,即 app. 在非webpack 模式下也可以用$set 方法,例如 app. $set(...)

    另一种方法:

    1
    2
    3
    4
    app.books.splice(3, 1, {
    name: '《CSS揭秘》',
    author: '[希] Lea Verou'
    })

    第二个问题也可以直接用splice 来解决:

    1
    app.books.splice(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
    <div id="app">
    <ul>
    <template v-for="book in filterBooks">
    <li>书名: {{book.name}}</li>
    <li>作者: {{book.author}}</li>
    </template>
    </ul>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    books: [
    {
    name: '《Vue.js 实战》',
    author: '梁灏'
    },
    {
    name: '《Vue.js 实战》'
    author: 'Douglas Crockford'
    },
    {
    name: '《Vue.js 实战》',
    author: 'Nicholas C.Zakas'
    },
    ]
    },
    computed: {
    filterBooks: function() {
    return this.books.filter(function (book) {
    return book.name.match(/JavaScript/);
    })
    }
    }
    });
    </script>

    上例是把书名中包含 JavaScript 关键词的数据过滤出来,计算属性 filterBooks 依赖 books ,但是不会修改 books 。实现排序也是类似的,比如在此基础上新加一个计算属性 sortedBooks ,按照书名的长度由长到短进行排序:

    1
    2
    3
    4
    5
    6
    7
    computed: {
    sortedBooks: function() {
    return this.books.sort(function(a, b) {
    return a.name.length < b.name.length;
    });
    }
    }

    在 Vue.js 2.x 中废弃了 1.x 中内直的 limitBy 、filterBy 和 orderBy 过滤器,统一改用计算属性来实现。

    方法与事件

    基本用法

    在第 2.2 节,我们已经引入了 Vue 事件处理的概念 v-on,在事件绑定上,类似原生 JavaScript的 onclick 等写法,也是在 HTML 上进行监听的。例如,我们监听一个按钮的点击事件,设置一个计数器,每次点击都加1:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div id="app">
    点击次数:{{counter}}
    <button @click="counter++">+ 1</button>
    </div>
    <script>
    new Vue({
    el: '#app',
    data: {
    counter: 0
    }
    })
    </script>

    @click 的表达式可以直接使用 JavaScript 语句,也可以是一个在 Vue 实例中 methods 选项内的函数名,比如对上例进行扩展,再增加一个按钮,点击一次,计数器加 10:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <div id="app">
    点击次数:{{counter}}
    <button @click="handleAdd()">+ 1</button>
    <button @click="handleAdd(10)"> + 10</button>
    </div>
    <script>
    new Vue({
    el: '#app',
    data: {
    counter: 0
    },
    methods: {
    handleAdd: function (count) {
    count = count || 1;
    // this 只想当前 Vue 实例 app
    this.counter += counter;
    }
    }
    })
    </script>

    在methods 中定义了我们需要的方法供 @click 调用, 需要注意的是,@click 调用的方法名后可以不跟括号”()” 。此时,如果该方法有参数,默认会将原生事件对象 event 传入,可以尝试修改为 @click=” handleAdd “,然后在 handleAdd 内打印出 count 参数看看。在大部分业务场景中,如果方法不需要传入参数,为了简便可以不写括号。

    这种在 HTML 元素上监昕事件的设计看似将 DOM 与 JavaScript 紧藕合,违背分离的原理,实则刚好相反。因为通过 HTML 就可以知道调用的是哪个方法,将逻辑与 DOM 解耦,便于维护。最重要的是, 当 ViewModel 销毁时,所有的事件处理器都会自动删除,无须自己清理。

    Vue 提供了一个特殊变量 $event ,用于访问原生 DOM 事件,例如下面的实例可以阻止链接打开:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <div id="app">
    <a href="http://www.apple.com" @click="HandleClick('禁止打开', $event)">打开链接</a>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    methods: {
    handleClick: function(message, event) {
    event.preventDefault();
    window.alert(message);
    }
    }
    })
    </script>

    修饰符

    在上例使用的 event.preventDefault() 也可以用 Vue 事件的修饰符来实现,在@绑定的事件后加小圆点”.”,再跟一个后缀来使用修饰符。Vue 支持以下修饰符:

    • .stop
    • .prevent
    • .capture
    • .self
    • .once

    具体用法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!--阻止单击事件冒泡-->
    <a @click.stop="handle"></a>
    <!--提交事件不再重载页面-->
    <form @submit.prevent="handle"></form>
    <!--修饰符可以串联-->
    <a @click.stop.prevent="handle"></a>
    <!--只有修饰符-->
    <form @submit.prevent></form>
    <!--添加事件侦听器时使用事件捕获模式-->
    <div @click.capture="handle">...</div>
    <!--只当事件在该元素本身(而不是子元素) 触发时触发回调-->
    <div @click.self="handle">...</div>
    <!--只触发一次,组件同样适用-->
    <a @click.once="handle"></a>

    在表单元素上监昕键盘事件时,还可以使用按键修饰符,比如按下具体某个键时才调用方法:

    1
    2
    <!--只有在 keyCode 是 13 时调用 vm.submit -->
    <input @keyup.13="submit">

    也可以自己配置具体按键:

    1
    2
    Vue.config.keyCodes.f1 = 112;
    // 全局定义后,就可以使用自 keyup.f1

    除了具体的某个 keyCode 外, Vue 还提供了一些快捷名称,以下是全部的别名:

    • .enter
    • .tab
    • .delete (捕获“删除”和“退格”键)
    • .esc
    • .space
    • .up
    • .down
    • .left
    • .right

    这些按键修饰符也可以组合使用,或和鼠标一起配合使用:

    • .ctrl
    • .alt
    • .shift
    • .meta (Mac 下是Command 键, Windows 下是窗口键)

    例如:

    1
    2
    3
    4
    <!-- Shift + S -->
    <input @keyup.shift.83="handleSSave">
    <!-- Ctrl + Click -->
    <div @click.ctrl="doSomething">Do something</div>

    以上就是事件指令 v-on 的基本内容,在第 7 章的组件中,我们还将介绍用 v-on 来绑定自定义事件。

    实战:利用计算属性、指令等知识开发购物车

    前 5 章内容基本涵盖了 Vue.js 最核心和常用的知识点,掌握这些内容己经可以上手开发一些小功能了。本节则以 Vue.js 的计算属性、内置指令、方法等内容为基础,完成一个在业务中具有代表性的小功能:购物车。

    在开始写代码前,要对需求进行分析,这样有助于我们理清业务逻辑,尽可能还原设计与产品交互。

    购物车需要展示一个己加入购物车的商品列表,包含商品名称、商品单价、购买数量和操作等信息,还需要实时显示购买的总价。其中购买数量可以增加或减少,每类商品还可以从购物车中移除。最终实现的效果如图5-6 所示。

    image-20220208143344081

    图5-6 购物车效果图

    在明确需求后,我们就可以开始编程了,因为业务代码较多,这次我们将 HTML 、css 、JavaScript 分离为3 个文件,便于阅读和维护:

    • index.html (引入资源及模板)
    • index.js (Vue 实例及业务代码)
    • style.css (样式)

    先在 index.html 中引入Vue.js 和相关资源,创建一个根元素来挂载 Vue 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>购物车实例</title>
    <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
    <div id="app" v-cloak>

    </div>
    <script src="https://unpkg.com/vue/dist/vue.min.js"></script>
    <script src="index.js"></script>
    </body>
    </html>

    注意,这里将 vue.min.js 和 index.js 文件写在 <body>的最底部,如果写在<head>里, Vue 实例将无法创建,因为此时 DOM 还没有被解析完成,除非通过异步或在事件 DOMContentLoaded (IE是 onreadystatechange )触发时再创建Vue 实例,这有点像 jQuery 的$(document).ready() 方法。

    本例需要用到 Vue.js 的 computed 、methods 等选项,在 index.js 中先初始化实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var app = new Vue({
    el: '#app',
    data: {

    },
    computed: {

    },
    methods: {

    }
    });

    我们需要的数据比较简单,只有一个列表,里面包含了商品名称、单价、购买数量。在实际业务中,这个列表应该是通过Ajax 从服务端动态获取的,这里只做示例,所以直接写入在 data 选项内,另外每个商品还应该有一个全局唯一的 id 。我们在 data 内写入列表 list:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    data: {
    list: [
    {
    id: 1,
    name: 'iPhone 7',
    price: 6188,
    count: 1
    },
    {
    id: 2,
    name: 'ipad Pro',
    price: 5888,
    count: 1
    },
    {
    id: 3,
    name: 'MacBook Pro',
    price: 21488,
    count: 1
    }
    ]
    }

    数据构建好后,可以在 index.html 中展示列表了,毫无疑问,肯定会用到 v-for ,不过在此之前,我们先做一些小的优化。因为每个商品都是可以从购物车移除的,所以当列表为空时,在页面中显示一个“购物车为空”的提示更为友好,我们可以通过判断数组 list 的长度来实现该功能:

    1
    2
    3
    4
    5
    6
    <div id="app" v-cloak>
    <template v-if="list.length">

    </template>
    <div v-else>购物车为空</div>
    </div>

    <template> 里的代码分两部分, 一部分是商品列表信息,我们用表格 table 来展现; 另一部分就是带有千位分隔符的商品总价(每隔三位数加进一个逗号〉。这部分代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <template v-if="list.length">
    <table>
    <thead>
    <tr>
    <th></th>
    <th>商品名称</th>
    <th>商品单价</th>
    <th>购买数量</th>
    <th>操作</th>
    </tr>
    </thead>
    <tbody>

    </tbody>
    </table>
    <div>总价:¥ {{totalPrice}}</div>
    </template>

    总价 totalPrice 是依赖于商品列表而动态变化的,所以我们用计算属性来实现,顺便将结果转换为带有“千位分隔符”的数字,在 index.js 的 computed 选项内写入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    computed: {
    totalPrice: function () {
    var total = 0;
    for(var i = 0; i , this.list.length; i++) {
    var item = this.list[i];
    total += item.price * item.count;
    }
    return total.toString().replace(/\B(?=(\d{3})+$)/g,',');
    }
    }

    这段代码难点在于千位分隔符的转换,读者可以查阅正则匹配的相关内容后尝试了解 replace()的正则含义。

    最后就剩下商品列表的渲染和相关的几个操作了。先在 <tbody> 内把数组 list 用 v-for 指令循环出来:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <tbody>
    <tr v-for="(item, index) in list">
    <td>{{index + 1}}</td>
    <td>{{item.name}}</td>
    <td>{{item.price}}</td>
    <td>
    <button @click="handleReduce(index)" :disabled="item.count === 1">-</button>
    {{item.count}}
    <button @click="handleAdd(index)">+</button>
    </td>
    <td>
    <button @click="handleRemove(index)">移除</button>
    </td>
    </tr>
    </tbody>

    商品序号、名称、单价、数量都是直接使用插值来完成的,在第 4 列的两个按钮 <button> 用于增/减购买数量,分别绑定了两个方法 handleReduce 和handleAdd ,参数都是当前商品在数组 list 中的索引。很多时候, 一个元素上会同时使用多个特性(尤其是在组件中使用 props 传递数据时),写在一行代码较长,不便阅读,所以建议特性过多时, 将每个特性都单独写为一行,比如第一个 <button> 中使用了 v-bind 和 v-on 两个指令(这里都用的语法糖写法〉。每件商品购买数量最少是 1 件,所以当 count 为 1 时,不允许再继续减少,所以这里给 <button> 动态绑定了 disabled 特性来禁用按钮。

    在 index.js 中继续完成剩余的 3 个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    methods: {
    handleReduce: function (index) {
    if (this.list[index].count === 1) return;
    this.list[index].count--;
    },
    handleAdd: function (index) {
    this.list[index].count++;
    },
    handleRemove: function (index) {
    this.list.splice(index, 1);
    }

    这 3 个方法都是直接对数组 list 的操作, 没有太复杂的逻辑。需要说明的是, 虽然在 button 上己经绑定了 disabled 特性, 但是在 handleReduce 方法内又判断了一遍,这是因为在某些时候, 可能不一定会用 button 元素,也可能是 div 、span 等,给它们增加 disabled 是没有任何作用的,所以安全起见,在业务逻辑中再判断一次,避免因修改 HTML 模板后出现 bug 。

    以下是购物车示例的完整代码:

    index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>购物车实例</title>
    <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
    <div id="app" v-cloak>
    <template v-if="list.length">
    <table>
    <thead>
    <tr>
    <th></th>
    <th>商品名称</th>
    <th>商品单价</th>
    <th>购买数量</th>
    <th>操作</th>
    </tr>
    </thead>
    <tbody>
    <tr v-for="(item, index) in list">
    <td>{{index + 1}}</td>
    <td>{{item.name}}</td>
    <td>{{item.price}}</td>
    <td>
    <button @click="handleReduce(index)" :disabled="item.count === 1">-</button>
    {{item.count}}
    <button @click="handleAdd(index)">+</button>
    </td>
    <td>
    <button @click="handleRemove(index)">移除</button>
    </td>
    </tr>
    </tbody>
    </table>
    <div>总价:¥ {{totalPrice}}</div>
    </template>
    <div v-else>购物车为空</div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script src="index.js"></script>
    </body>
    </html>

    index.js

    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
    var app = new Vue({
    el: '#app',
    data: {
    list: [
    {
    id: 1,
    name: 'iPhone 7',
    price: 6188,
    count: 1
    },
    {
    id: 2,
    name: 'ipad Pro',
    price: 5888,
    count: 1
    },
    {
    id: 3,
    name: 'MacBook Pro',
    price: 21488,
    count: 1
    }
    ]
    },
    computed: {
    totalPrice: function () {
    var total = 0;
    for(var i = 0; i < this.list.length; i++) {
    var item = this.list[i];
    total += item.price * item.count;
    }
    return total.toString().replace(/\B(?=(\d{3})+$)/g,',');
    }
    },
    methods: {
    handleReduce: function (index) {
    if (this.list[index].count === 1) return;
    this.list[index].count--;
    },
    handleAdd: function (index) {
    this.list[index].count++;
    },
    handleRemove: function (index) {
    this.list.splice(index, 1);
    }
    }
    });

    style.css:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    [v-clock] {
    display: none;
    }

    table {
    border: 1px solid #e9e9e9;
    border-collapse: collapse;
    border-spacing: 0;
    empty-cells: show;
    }

    th, td {
    padding: 8px 16px;
    border: 1px solid #e9e9e9;
    text-align: left;
    }

    th {
    background: #f7f7f7;
    color: #5c6b77;
    font-weight: 600;
    white-space:nowrap;
    }

    练习1: 在当前示例基础上扩展商品列表,新增一项是否选中该商品的功能,总价变为只计算选中商品的总价, 同时提供一个全选的按钮。

    练习2: 将商品列表 list 改为一个二维数组来实现商品的分类,比如可分为“电子产品”“生活用品”和“果蔬” , 同类商品聚合在一起。提示,你可能会用到两次 v-for。

    表单与v-model

    表单类控件承载了一个网页数据的录入与交互,本章将介绍如何使用指令 v-model 完成表单的数据双向绑定。

    基本用法

    表单控件在实际业务较为常见,比如单选、多选、下拉选择、输入框等,用它们可以完成数据的录入、校验、提交等。Vue.js 提供了 v-model 指令,用于在表单类元素上双向绑定数据,例如在输入框上使用时,输入的内容会实时映射到绑定的数据上。例如下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div id="app">
    <input type="text" v-model="message" placeholder="输入...">
    <p>输入的内容是: {{message}}</p>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    message: ''
    }
    })
    </script>

    在输入框输入的问时,{{message}}也会实时将内容渲染在视图中,如图 6 -1 所示。

    image-20220206140823288

    图6-1 v-model 指令对数据的双向绑定

    对于文本域 textarea 也是同样的用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <div id="app">
    <textarea v-model="text" placeholder="输入..."></textarea>
    <p>输入的内容是:</p>
    <p style="white-space: pre">{{text}}</p>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    text: ''
    }
    })
    </script>

    使用 v-model 后,表羊控件显示的值只依赖所绑定的数据,不再关心初始化时的 value 属性,对于在 <textarea></textarea> 之间插入的值,也不会生效。

    使用 v-model 时,如果是用中文输入法输入中文,一般在没有选定饲组前,也就是在拼音阶段, Vue 是不会更新数据的,当敲下汉字时才会触发更新。如果希望总是实时更新,可以用 @input 来替代v-model。 事实上, v-model 也是一个特殊的语法糖,只不过它会在不同的表单上智能处理。例如下面的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <div id="app">
    <input type="text" @input="handleInput" placeholder="输入...">
    <p>输入的内容是:{{message}}</p>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    message: ''
    },
    methods: {
    handleInput: function (e) {
    this.message = e.target.value;
    }
    }
    })
    </script>

    来看看更多的表单控件。

    单选按钮:

    单选按钮在单独使用时,不需要 v-model ,直接使用 v-bind 绑定一个布尔类型的值, 为真时选中, 为否时不选,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div id="app">
    <input type="radio" :checked="picked">
    <label>单选按钮</label>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    picked: true
    }
    })
    </script>

    如果是组合使用来实现互斥选择的效果,就需要 v-model 配合 value 来使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <div id="app">
    <input type="radio" v-model="picked" value="html" id="html">
    <label for="html">HTML</label>
    <br>
    <input type="radio" v-model="picked" value="js" id="js">
    <label for="js">JavaScript</label>
    <br>
    <input type="radio" v-model="picked" value="css" id="css">
    <label for="css">CSS</label>
    <br>
    <p>选择的项是: {{ picked }}</p>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    picked: 'js'
    }
    })
    </script>

    数据 picked 的值与单选按钮的 value 值一致时,就会选中该项,所以当前状态下选中的是第二项 JavaScript ,如图 6-2 所示。

    image-20220209091108498

    图 6-2 单选按钮示例结果

    复选框:

    复选框也分单独使用和组合使用,不过用法稍与单选不同。复选框单独使用时,也是用 v-model 来绑定一个布尔值,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div id="app">
    <input type="checkbox" v-model="checked" id="checked">
    <label for="checked">选择状态: {{checked}}</label>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    checked: false
    }
    })
    </script>

    在句选时,数据 checked 的值变为了 true, label 中渲染的内容也会更新。

    组合使用时,也是 v-model 与 value 一起,多个勾选框都绑定到同一个数组类型的数据, value 的值在数组当中,就会选中这一项。这一过程也是双向的,在勾选时, value 的值也会自动 push 到这个数组中,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <div id="app">
    <input type="checkbox" v-model="checked" value="html" id="html">
    <label for="html">HTML</label>
    <br>
    <input type="checkbox" v-model="checked" value="js" id="js">
    <label for="js">JavaScript</label>
    <br>
    <input type="checkbox" v-model="checked" value="css" id="css">
    <label for="css">CSS</label>
    <br>
    <p>选择的项是:{{checked}}</p>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    checked: ['html', 'css']
    }
    })
    </script>

    当前状态下的结果如图 6-3 所示:

    image-20220209091804442

    图6-3 多选框组合使用的结果

    选择列表:

    选择列表就是下拉选择器,也是常见的表单控件,同样也分为单选和多选两种方式。先看一下单选的示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <div id="app">
    <select v-model="selected">
    <option>html</option>
    <option value="js">JavaScript</option>
    <option>css</option>
    </select>
    <p>选择的项是:{{selected}}</p>
    </div>
    <script>
    var app = new Vue({
    el: '#app',
    data: {
    selected: 'html'
    }
    })
    </script>