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 所示。
图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 生态有所了解了。
图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 所示。
图 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);
除了显式地声明数据外,也可以指向一个已有的变量,并且它们之间默认建立了双向绑定,当修改其中任意一个时,另一个也会一起变化:
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); myData.a = 3 ; console .log(app.a)
生命周期 每个 Vue 实例创建时,都会经历一系列的初始化过程,同时也会调用相应的生命周期钩子,我们可以利用这些钩子,在合适的时机执行我们的业务逻辑。如果你使用过 jQuery , 一定知道它的 ready() 方法,比如以下示例:
1 2 3 $(document ).ready(function ( ) { });
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); }, mounted : function ( ) { console .log(this .$el) } })
插值与表达式 使用双大括号( 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 ( ) { 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>元素被复用了。
图5-1 切换前的状态
图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 所示。
图 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 所示。
图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 所示。
图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 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 .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 <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 <input @keyup.shift.83 ="handleSSave" > <div @click.ctrl ="doSomething" > Do something</div >
以上就是事件指令 v-on 的基本内容,在第 7 章的组件中,我们还将介绍用 v-on 来绑定自定义事件。
实战:利用计算属性、指令等知识开发购物车 前 5 章内容基本涵盖了 Vue.js 最核心和常用的知识点,掌握这些内容己经可以上手开发一些小功能了。本节则以 Vue.js 的计算属性、内置指令、方法等内容为基础,完成一个在业务中具有代表性的小功能:购物车。
在开始写代码前,要对需求进行分析,这样有助于我们理清业务逻辑,尽可能还原设计与产品交互。
购物车需要展示一个己加入购物车的商品列表,包含商品名称、商品单价、购买数量和操作等信息,还需要实时显示购买的总价。其中购买数量可以增加或减少,每类商品还可以从购物车中移除。最终实现的效果如图5-6 所示。
图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 所示。
图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 所示。
图 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 所示:
图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 >
是备选工页,如果含有 value 属性, v-model 就会优先匹配 value 的值;如果没有, 就会直接匹配 的text ,比如选中第二项时, selected 的值是js , 而不是JavaScript 。
给 添加属性 multiple 就可以多选了, 此时 v -model 绑定的是一个数组, 与复选框用法类似,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <div id ="app" > <select v-model ="selected" multiple > <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' , 'js' ] } }) </script >
在业务中, 经常用 v-for 动态输出, value 和 text 也是用 v-bind 来动态输出的, 例如:
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 <div id ="app" > <select v-model ="selected" > <option v-for ="option in options" :value ="option.value" > {{option.text}}</option > </select > <p > 选择的项是:{{selected}}</p > </div > <script > var app = new Vue({ el : '#app' , data : { selected : 'html' , options : [ { text : 'HTML' , value : 'html' }, { text : 'JavaScript' , value : 'js' }, { text : 'CSS' , value : 'css' } ] } }) </script >
虽然用选择列表 控件可以很简单地完成下拉选择的需求,但是在实际业务中反而不常用,因为它的样式依赖平台和浏览器,无法统一, 也不太美观, 功能也受限, 比如不支持搜索,所以常见的解决方案是用 div 模拟一个类似的控件。当阅读完第 7 章组件的内容后, 可以尝试编写一个下拉选择器的通用组件。
绑定值 上一节介绍的单选按钮、复选框和选择列表在单独使用或单选的模式下, v-model 绑定的值是一个静态字符串或布尔值, 但在业务中,有时需要绑定一个动态的数据, 这时可以用 v-bind 来实现。
单选按钮:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <div id ="app" > <input type ="radio" v-model ="picked" :value ="value" > <label > 单选按钮</label > <p > {{picked}}</p > <p > {{value}}</p > </div > <script > var app = new Vue({ el : '#app' , data : { picked : false , value : 123 } }) </script >
在选中时, app.picked === app .value , 值都是123 。
复选框:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <div id ="app" > <input type ="checkbox" v-model ="toggle" :true-value ="value1" :false-value ="value2" > <label > 复选框</label > <p > {{toggle}}</p > <p > {{value1}}</p > <p > {{value2}}</p > </div > <script > var app = new Vue({ el : '#app' , data : { toggle : false , value1 : 'a' , value2 : 'b' } }) </script >
勾选时, app.toggle = app .value1 : 未勾选时, app.toggle = app .value2 。
选择列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div id ="app" > <select v-model ="selected" > <option :value ="{number: 123}" > 123</option > </select > {{selected.number}} </div > <script > var app = new Vue({ el : '#app' , data : { selected : '' } }) </script >
当选中时, app.selected 是一个 Object ,所以 app.selected.number == 123 。
修饰符 与事件的修饰符类似, v-model 也有修饰符,用于控制数据同步的时机。
.lazy:
在输入框中, v-model 默认是在 input 事件中同步输入框的数据(除了提示中介绍的中文输入法情况外),使用修饰符 .lazy 会转变为在 change 事件中同步,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 <div id ="app" > <input type ="text" v-model.lazy ="message" > <p > {{message}}</p > </div > <script > var app = new Vue({ el : '#app' , data : { message : '' } }) </script >
这时, message 并不是实时改变的,而是在失焦或按回车时才更新。
.number:
使用修饰符 .number 可以将输入转换为 Number 类型,否则虽然你输入的是数字,但它的类型其实是 String ,比如在数字输入框时会比较有用,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 <div id ="app" > <input type ="number" v-model.number ="message" > <p > {{typeof message}}</p > </div > <script > var app = new Vue({ el : '#app' , data : { message : 123 } }) </script >
.trim:
修饰符 .trim 可以自动过滤输入的首尾空格,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 <div id ="app" > <input type ="text" v-model.trim ="message" > <p > {{message}}</p > </div > <script > var app = new Vue({ el : '#app' , data : { message : '' } }) </script >
从 Vue.js 2.x 开始, v-model 还可以用于自定义组件,满足定制化的需求, 在第 7 章会详细介绍。
组件详解 组件( Component )是Vue. 最核心的功能,也是整个框架设计最精彩的地方,当然也是最难掌握的。本章将带领你由浅入深地学习组件的全部内容,并通过几个实战项目熟练使用Vue 组件。
组件与复用 为什么使用组件 在正式介绍组件前,我们先来看一个简单的场景,如图 7-1 所示。
图 7-1 常见的聊天界面
图 7-1 中是一个很常见的聊天界面,有一些标准的控件,比如右上角的关闭按钮、输入框、发送按钮等。你可能要问了,这有什么难的,不就是几个 div 、input 吗?好,那现在需求升级了,这几个控件还有别的地方要用到。没问题,复制粘贴呗。那如果输入框要带数据验证,按钮的图标支持自定义呢?这样用 JavaScript 封装后一起复制吧。那等到项目快完结时,产品经理说,所有使用输入框的地方,都要改成支持回车键提交。好吧,给我一天的时间,我一个一个加上去。
上面的需求虽然有点变态,但却是业务中很常见的,那就是一些控件、JavaScript 能力的复用。没错, Vue.js 的组件就是提高重用性的,让代码可复用,当学习完组件后,上面的问题就可以分分钟搞定了,再也不用害怕产品经理的奇葩需求。
我们先看一下图 7-1 中的示例用组件来编写是怎样的,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <Card sytle ="width: 350px;" > <p slot ="title" > 与XXX聊天中</p > <a href ="#" slot ="extra" > <Icon type ="android-close" size ="18" > </Icon > </a > <div style ="height: 100px;" > </div > <div > <Row :gutter ="16" > <i-col span ="17" > <i-input v-model ="value" placeholder ="请输入..." > </i-input > </i-col > <i-col span ="4" > <i-button type ="primary" icon ="paper-airplane" > 发送</i-input > </i-col > </Row > </div > </Card >
是不是很奇怪,有很多我们从来都没有见过的标签,比如 、、、和 等,而且整段代码除了内联的几个样式外, 一句css 代码也没有,但最终实现的 UI 就是图 7-1 的效果。
这些没见过的自定义标签就是组件,每个标签代表一个组件,在任何使用 Vue 的地方都可以直接使用。接下来,我们就来看看组件的具体用法。
组件用法 回顾一下我们创建Vue 实例的方法:
1 2 3 var app = new Vue({ el : '#app' })
组件与之类似,需要注册后才可以使用。注册有全局注册和局部注册两种方式。全局注册后,任何 Vue 实例都可以使用。全局注册示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 <div id ="app" > <my-component > </my-component > </div > <script > Vue.component('my-component' , { }); var app = new Vue({ el : '#app' }) </script >
此时打开页面还是空白的,因为我们注册的组件没有任何内容,在组件选项中添加 template 就可以显示组件内容了, 示例代码如下:
1 2 3 Vue.component('my-component' , { template : '<div>这是组件内容</div>' });
渲染后的结果是:
1 2 3 <div id ="app" > <div > 这是组件内容</div > </div >
template 的DOM 结构必须被一个元素包含, 如果直接写成 “这里是组件的内容”, 不带“
”是无法渲染的。
在 Vue 实例中,使用 components 选项可以局部注册组件,注册后的组件只有在该实例作用域下有效。组件中也可以使用 components 选项来注册组件,使组件可以嵌套。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div id ="app" > <my-component > </my-component > </div > <script > var Child = { template : '<div>局部注册组件的内容</div>' } var app = new Vue({ el : '#app' , components : { 'my-component' : Child } }) </script >
Vue 组件的模板在某些情况下会受到 HTML 的限制,比如
内规定只允许是 , , 等这些表格元素,所以在 内直接使用组件是无效的。这种情况下,可以使用特殊的 is 属性来挂载组件, 示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <div id ="app" > <table > <tbody is ="my-component" > </tbody > </table > </div > <script > Vue.component('my-component' , { template : '<div>这里是组件的内容</div>' }); var app = new Vue({ el : '#app' , components : { 'my-component' : Child } }) </script >
tbody 在渲染时, 会被替换为组件的内容。常见的限制元素还有
如果使用的是字符串模板,是不受限制的,比如后面章节介绍的 .vue 单文件用法等。
除了 template 选项外,组件中还可以像 Vue 实例那样使用其他的选项,比如 data 、computed 、methods 等。但是在使用data 时, 和实例稍有区别, data 必须是函数,然后将数据 return 出去,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <div id ="app" > <my-component > </my-component > </div > <script > Vue.component('my-component' , { template : '<div>{{message}}</div>' , data : function ( ) { return { message : '组件内容' } } }); var app = new Vue ({ el : '#app' }) </script >
JavaScript 对象是引用关系, 所以如果 return 出的对象引用了外部的一个对象, 那这个对象就是共享的, 任何一方修改都会同步。比如下面的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <div id ="app" > <my-component > </my-component > <my-component > </my-component > <my-component > </my-component > </div > <script > var data = { counter : 0 }; Vue.component('my-component' , { template : '<button @click="counter++">{{counter}}</button>' , data : function ( ) { return data; } }); var app = new Vue({ el : '#app' }) </script >
组件使用了 3 次, 但是点击任意一个 , 3 个的数字都会加 1 ,那是因为组件的 data 引用的是外部的对象,这肯定不是我们期望的效果, 所以给组件返回一个新的 data 对象来独立, 示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div id ="app" > <my-component > </my-component > <my-component > </my-component > <my-component > </my-component > </div > <script > Vue.component('my-component' , { template : '<button @click="counter++">{{counter}}</button>' , data : function ( ) { return { counter : 0 } } }); var app = new Vue({ el : '#app' }) </script >
这样,点击 3 个按钮就互不影响了,完全达到复用的目的。
使用props 传递数据 基本用法 组件不仅仅是要把模板的内容进行复用,更重要的是组件间要进行通信。通常父组件的模板中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接收到后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程就是通过 props 来实现的。
在组件中,使用选项 props 来声明需要从父级接收的数据, props 的值可以是两种, 一种是字符串数组,一种是对象, 本小节先介绍数组的用法。比如我们构造一个数组,接收一个来自父级的数据 message ,并把它在组件模板中渲染,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 <div id ="app" > <my-component message ="来自父组件的数据" > </my-component > </div > <script > Vue.component('my-component' , { props : ['message' ], template : '<div>{{message}}</div>' }); var app = new Vue({ el : '#app' }) </script >
渲染后的结果为:
1 2 3 <div id ="app" > <div > 来自父组件的数据</div > </div >
props 中声明的数据与组件 data 函数 return 的数据主要区别就是 props 的来自父级,而 data 中的是组件自己的数据,作用域是组件本身,这两种数据都可以在模板 template 及计算属性 computed 和方法 methods 中使用。上例的数据 message 就是通过 props 从父级传递过来的,在组件的自定义标签上直接写该 props 的名称,如果要传递多个数据,在 props 数组中添加项即可。
由于 HTML 特性不区分大小写,当使用 DOM 模板时,驼峰命名 (camelCase) 的 props 名称要转为短横分隔命名 (kebab-case),例如:
1 2 3 4 5 6 7 8 9 10 11 12 <div id ="app" > <my-component warning-text ="提示信息" > </my-component > </div > <script > Vue.component('my-component' , { props : ['warningText' ], template : '<div>{{warningText}}</div>' }); var app = new Vue({ el : '#app' }) </script >
如果使用的是字符串模板,仍然可以忽略这些限制。
有时候,传递的数据并不是直接写死的,而是来自父级的动态数据,这时可以使用指令 v-bind 来动态绑定 props 的值,当父组件的数据变化时,也会传递给子组件。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <div id ="app" > <input type ="text" v-model ="parentMessage" > <my-component :message ="parentMessage" > </my-component > </div > <script > Vue.component('my-component' , { props : ['message' ], template : '<div>{{message}}</div>' }); var app = new Vue ({ el : '#app' , data : { parentMessage : '' } }) </script >
这里用 v-model 绑定了父级的数据 parentMessage ,当通过输入框任意输入时,子组件接收到的 props “message “也会实时响应,并更新组件模板。
注意,如果你要直接传递数字、布尔值、数组、对象,而且不适用 v-bind,传递的仅仅是字符串,尝试下面的示例来对比:
1 2 3 4 5 6 7 8 9 10 11 12 13 <div id ="app" > <my-component message ="[1,2,3]" > </my-component > <my-component :message ="[1,2,3]" > </my-component > </div > <script > Vue.component('my-component' , { props : ['message' ], template : '<div>{{message.length}}</div>' }); var app = new Vue({ el : '#app' }) </script >
同一个组件使用了两次,区别仅仅是第二个使用的是 v-bind ,渲染后的结果,第一个是 7 ,第二个才是数组的长度 3。
单向数据流 Vue 2.x 与 Vue 1.x 比较大的一个改变就是, Vue2.x 通过 props 传递数据是单向的了, 也就是父组件数据变化时会传递给子组件,但是反过来不行。而在 Vue 1.x 里提供了 .sync 修饰符来支持双向绑定。之所以这样设计,是尽可能将父子组件解稿,避免子组件无意中修改了父组件的状态。
业务中会经常遇到两种需要改变 prop 的情况,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件 data 内再声明一个数据,引用父组件的 prop ,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <div id ="app" > <my-component :init-count ="1" > </my-component > </div > <script > Vue.component('my-component' , { props : ['initCount' ], template : '<div>{{count}}</div>' , data : function ( ) { return { count : this .initCount } } }); var app = new Vue({ el : '#app' }) </script >
组件中声明了数据 count , 它在组件初始化时会获取来自父组件的 initCount , 之后就与之无关了,只用维护 count , 这样就可以避免直接操作 initCount 。
另一种情况就是 prop 作为需要被转变的原始值传入。这种情况用计算属性就可以了, 示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <div id ="app" > <my-component :width ="100" > </my-component > </div > <script > Vue.component('my-component' , { props : ['width' ], template : '<div>组件内容</div>' , computed : function ( ) { style : function ( ) { return { width : this .width + 'px' } } } }); var app = new Vue({ el : '#app' }) </script >
因为用 css 传递宽度要带单位 (px),但是每次都写太麻烦,而且数值计算一般是不带单位的,所以统一在组件内使用计算属性就可以了。
注意,在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,所以 props 是对象和数组时,在子组件内改变是会影响父组件的。
数据验证 我们上面所介绍的 props 选项的值都是一个数组,一开始也介绍过,除了数组外,还可以是对象,当 prop 需要验证时,就需要对象写法。
一般当你的组件需要提供给别人使用时,推荐都进行数据验证,比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。
以下是几个 prop 的示例:
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 Vue.component('my-component' , { props : { propA : Number , propB : [String , Number ], propC : { type : Boolean , default : true }, propD : { type : Number , required : true }, propE : { type : Array , default : function ( ) { return []; } }, propF : { validator : function (value ) { return value > 10 ; } } } })
验证的 type 类型可以是:
String
Number
Boolean
Object
Array
Function
type 也可以是一个自定义构造器,使用 instanceof 检测。当prop 验证失败时,在开发版本下会在控制台抛出一条警告。
组件通信 我们已经知道,从父组件向子组件通信,通过 props 传递数据就可以了,但 Vue 组件通信的场景不止有这一种,归纳起来,组件之间通信可以用图 7-2 表示。
图7-2 组件通信示例
组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信。本节将介绍各种组件之间通信的方法。
自定义事件 当子组件需要向父组件传递数据时,就要用到自定义事件。我们在介绍指令 v-on 时有提到,v-on 除了监听 DOM 事件外,还可以用于组件之间的自定义事件。
如果你了解过 JavaScript 的设计模式一一观察者模式, 一定知道 dispatchEvent 和 addEventListener 这两个方法。Vue 组件也有与之类似的一套模式,子组件用$emit() 来触发事件,父组件用 $on() 来监昕子组件的事件。
父组件也可以直接在子组件的自定义标签上使用 v-on 来监昕子组件触发的自定义事件,示例代码如下:
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 <div id ="app" > <p > 总数:{{ total }}</p > <my-component @increase ="handleGetTotal" @reduce ="handleGetTotal" > </my-component > </div > <script > Vue.component('my-component' , { template : '\ <div>\ <button @click="handleIncrease">+1</button>\ <button @click="handleReduce">-1</button>\ </div>' , data : function ( ) { return { counter : 0 } }, methods : { handleIncrease : function ( ) { this .counter++; this .$emit('increase' , this .counter); }, handleReduce : function ( ) { this .counter--; this .$emit('reduce' , this .counter); } } }); var app = new Vue({ el : '#app' , data : { total : 0 }, methods : { handleGetTotal : function (total ) { this .total = total; } } }) </script >
上面示例中,子组件有两个按钮,分别实现加 1 和减 1 的效果, 在改变组件的data “counter” 后,通过 $emit() 再把它传递给父组件, 父组件用 v-on:increase 和 v-on:reduce (示例使用的是语法糖)。
$emit() 方法的第一个参数是自定义事件的名称, 例如示例的 increase 和 reduce 后面的参数都是要传递的数据,可以不填或填写多个。
除了用 v-on 在组件上监听自定义事件外,也可以监听 DOM 事件,这时可以用 .native 修饰符表示监听的是一个原生事件,监听的是该组件的根元素,示例代码如下:
1 <my-component v-on:click.native ="handleClick" > </my-component >
使用v-model Vue 2.x 可以在自定义组件上使用 v-model 指令,我们先来看一个示例:
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" > <p > 总数:{{ total }}</p > <my-component v-model ="total" > </my-component > </div > <script > Vue.component('my-component' , { template : '<button @click="handleClick">+1</button>' , data : function ( ) { return { counter : 0 } }, methods : { handleClick : function ( ) { this .counter++; this .$emit('input' , this .counter) } } }); var app = new Vue({ el : '#app' , data : { total : 0 } }) </script >
仍然是点击按钮加 1 的效果, 不过这次组件 $emit() 的事件名是特殊的 input, 在使用组件的父级,并没有在 上使用 @input=”handler”,而是直接用了 v-model 绑定的一个数据 total 。这也可以称作是一个语法糖,因为上面的示例可以间接地用自定义事件来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div id ="app" > <p > 总数:{{ total }}</p > <my-component @input ="handleGetTotal" > </my-component > </div > <script > var app = new Vue({ el : '#app' , data : { total : 0 }, methods : { handleGetTotal : function (total ) { this .total = total; } } }) </script >
v-model 还可以用来创建自定义的表单输入组件, 进行数据双向绑定,例如:
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 <div id ="app" > <p > 总数:{{ total }}</p > <my-component v-model ="total" > </my-component > <button @click ="handleReduce" > -1</button > </div > <script > Vue.component('my-component' , { props : ['value' ], template : '<input :value="value" @input="updateValue">' , methods : { updateValue : function (event ) { this .$emit('input' , event.target.value); } } }); var app = new Vue({ el : '#app' , data : { total : 0 }, methods : { handdleReduce : function ( ) { this .total--; } } }) </script >
实现这样一个具有双向绑定的v -model 组件要满足下面两个要求:
接收一个value 属性。
在有新的va lue 时触发input 事件。
非父子组件通信 在实际业务中,除了父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种,兄弟组件和跨多级组件。为了更加彻底地了解Vue.js 2.x 中的通信方法,我们先来看一下在Vue.js 1.x 中是如何实现的,这样便于我们了解 Vue.js 的设计思想。
在Vue.js 1.x 中,除了 $emit() 方法外,还提供了 $dispatch() 和 $broadcast() 这两个方法。$dispatch() 用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在 Vue 实例的 events 选项内接收,示例代码如下:
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" > {{message}} <my-component > </my-component > </div > <script > Vue.component('my-component' , { template : '<button @click="handleDispatch">派发事件</button>' , methods : { handleDispatch : function ( ) { this .$dispatch('on-message' , '来自内部组件的数据' ); } } }); var app = new Vue({ el : '#app' , data : { message : '' }, events : { 'on-message' : function (msg ) { this .message = msg; } } }) </script >
同理, $broadcast() 是由上级向下级广播事件的,用法完全一致,只是方向相反。
这两种方法一旦发出事件后,任何组件都是可以接收到的, 就近原则, 而且会在第一次接收到后停止冒泡,除非返回 true。
这两个方法虽然看起来很好用,但是在 Vue.js 2.x 中都废弃了, 因为基于组件树结构的事件流方式让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱,并且不能解决兄弟组件通信的问题。
在Vue.js 2.x 中, 推荐使用一个空的 Vue 实例作为中央事件总线 (bus),也就是一个中介。为了更形象地了解它,我们举一个生活中的例子。
比如你需要租房子, 你可能会找房产中介来登记你的需求, 然后中介把你的信息发给满足要求的出租者,出租者再把报价和看房时间告诉中介,由中介再转达给你,整个过程中, 买家和卖家并没有任何交流,都是通过中间人来传话的。
或者你最近可能要换房了, 你会找房产中介登记你的信息, 订阅与你找房需求相关的资讯,一旦有符合你的房子出现时,中介会通知你,并传达你房子的具体信息。
这两个例子中, 你和出租者担任的就是两个跨级的组件,而房产中介就是这个中央事件总线 (bus)。比如下面的示例代码:
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 <div id ="app" > {{message}} <component-a > </component-a > </div > <script > var bus = new Vue(); Vue.component('component-a' , { template : '<button @click="handleEvent">传递事件</button>' , methods : { handleEvent : function ( ) { bus.$emit('on-message' , '来自组件 component-a 的内容' ); } } }); var app = new Vue({ el : '#app' , data : { message : '' }, mounted : function ( ) { var _this = this ; bus.$on('on-message' , function (msg ) { _this.message = msg; }); } }) </script >
首先创建了一个名为 bus 的空 Vue 实例,里面没有任何内容;然后全局定义了组件 component-a ;最后创建 Vue 实例 app ,在 app 初始化时,也就是在生命周期 mounted 钩子函数里监听了来自 bus 的事件 on-message ,而在组件 component-a 中,点击按钮会通过 bus 把事件 on-message 发出去,此时 app 就会接收到来自 bus 的事件,进而在回调里完成自己的业务逻辑。
这种方法巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级,而且 Vue 1.x 和 Vue 2.x 都适用。如果深入使用,可以扩展 bus 实例,给它添加 data 、methods 、computed 等选项,这些都是可以公用的,在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息,比如用户登录的昵称、性别、邮箱等,还有用户的授权token 等。只需在初始化时让bus 获取一次,任何时间、任何组件就可以从中直接使用了,在单页面富应用 (SPA) 中会很实用,我们会在进阶篇里逐步介绍这些内容。
当你的项目比较大,有更多的小伙伴参与开发时,也可以选择更好的状态管理解决方案 vuex,在进阶篇里会详细介绍关于它的用法。
除了中央事件总线 bus 外,还有两种方法可以实现组件间通信:父链和子组件索引。
父链
在子组件中,使用 this.$parent 可以直接访问该组件的父实例或组件,父组件也可以通过 this.$children 访问它所有的子组件,而且可以递归向上或向下无线访问, 直到根实例或最内层的组件。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <div id ="app" > {{message}} <component-a > </component-a > </div > <script > Vue.component('component-a' , { template : '<button @click="handleEvent">通过父链直接修改数据</button>' , methods : { handleEvent : function ( ) { this .$parent.message = '来自组件 component-a 的内容' ; } } }); var app = new Vue ({ el : '#app' , data : { message : '' } }) </script >
尽管 Vue 允许这样操作,但在业务中, 子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧藕合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改, 理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过 props 和 $emit 来通信。
子组件索引
当子组件较多时, 通过 this.$children 来一一遍历出我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。Vue 提供了子组件索引的方法,用特殊的属性 ref 来为子组件指定一个索引名称,示例代码如下:
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" > <button #click ="handleRef" > 通过 ref 获取子组件实例</button > <component-a ref ="comA" > </component-a > </div > <script > Vue.component('component-a' , { template : '<div>子组件</div>' , data : function ( ) { return message: '子组件内容' } }); var app = new Vue({ el : '#app' , methods : { handleRef : function ( ) { var msg = this .$refs.comA.message; console .log(msg); } } }) </script >
在父组件模板中,子组件标签上使用ref 指定一个名称,井在父组件内通过 this.$refs 来访问指定名称的子组件。
$refs 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用 $refs。
与 Vue 1.x 不同的是, Vue 2.x 将 v-el 和 v-ref 合并为了 ref, Vue 会自动去判断是普通标签还是组件。可以尝试补全下面的代码,分别打印出两个 ref 看看都是什么:
1 2 3 4 <div id ="app" > <p ref ="p" > 内容</p > <child-component ref ="child" > </child-component > </div >
使用slot 分发内容 什么是slot 我们先看一个比较常规的网站布局,如图 -3 所示。
图 7-3 网站布局
这个网站由一级导航、二级导航、左侧列表、正文以及底部版权信息 5 个模块组成,如果要将它们都组件化,这个结构可能会是:
1 2 3 4 5 6 7 8 <app > <menu-main > </menu-main > <menu-sub > </menu-sub > <div class ="container" > <menu-left > </menu-left > <container > </container > </div > </app >
当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到 slot , 这个过程叫作内容分发( transclusion )。以 为例,它有两个特点:
组件不知道它的挂载点会有什么内容。挂载点的内容是由的父组件决定的.
组件很可能有它自己的模板。
props 传递数据、events 触发事件和 slot 内容分发就构成了 Vue 组件的 3 个 API 来源,再复杂的组件也是由这 3 部分构成的。
作用域 正式介绍 slot 前,需要先知道一个概念: 编译的作用域。比如父组件中有如下模板:
1 2 3 <child-component > {{message}} </child-component >
这里的 message 就是一个 slot ,但是它绑定的是父组件的数据,而不是组件 的数据。
父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。例如下面的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <div id ="app" > <child-component v-show ="showChild" > </child-component > </div > <script > Vue.component('child-component' , { template : '<div>子组件</div>' }); var app = new Vue ({ el : '#app' , data : { showChild : true } }) </script >
这里的状态 showChild 绑定的是父组件的数据,如果想在子组件上绑定,那应该是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <div id ="app" > <child-component > </child-component > </div > <script > Vue.component('child-component' , { template : '<div v-show="showChild">子组件</div>' , data : { showChild : true } }); var app = new Vue ({ el : '#app' }) </script >
因此, slot 分发的内容,作用域是在父组件上的。
slot 用法 单个 Slot
在子组件内使用特殊的 元素就可以为这个子组件开启一个 slot (插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的 标签及它的内容。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <div id ="app" > <child-component > <p > 分发的内容</p > <p > 更多分发的内容</p > </child-component > </div > <script > Vue.component('child-component', { template: '\ <div > \ <slot > \ <p > 如果父组件没有插入内容,我将默认出现</p > \ </slot > \ </div > ' }); var app = new Vue({ el: '#app' }) </script >
子组件 child-component 的模板内定义了一个 元素,并且用一个 作为默认的内容,在父组件没有使用 slot 时,会渲染这段默认的文本;如果写入了slot, 那就会替换整个 。所以上例渲染后的结果为:
1 2 3 4 5 6 <div id ="app" > <div > <p > 分发的内容</p > <p > 更多分发的内容</p > </div > </div >
注意,子组件 内的备用内容,它的作用域是子组件本身。
具名Slot
给 元素指定一个 name 后可以分发多个内容,具名 Slot 可以与单个 共存,例如下面的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <div id ="app" > <child-component > <h2 slot ="header" > 标题</h2 > <p > 正文内容</p > <p > 更多的正文内容</p > <div slot ="footer" > 底部信息</div > </child-component > </div > <script > Vue.component('child-component', { template: '\ <div class ="container" > \ <div class ="header" > <slot name ="header" > </slot > </div > \ <div class ="main" > <slot > </slot > </div > \ <div class ="footer" > <slot name ="footer" > </slot > </div > \ </div > ' }); var app = new Vue({ el: '#app' }) </script >
子组件内声明了 3 个 元素,其中在 <div class=”main” 与内的 没有使用 name 特性, 它将作为默认 slot 出现,父组件没有使用slot 特性的元素与内容都将出现在这里。
如果没有指定默认的匿名 slot ,父组件内多余的内容片段都将被抛弃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div id ="app" > <div class ="container" > <div class ="header" > <h2 > 标题</h2 > </div > <div class ="main" > <p > 正文内容</p > <p > 更多的正文内容</p > </div > <div class ="footer" > <div > 底部信息</div > </div > </div > </div >
在组合使用组件时,内容分发 API 至关重要。
作用域插槽 作用域插槽是一种特殊的 slot ,使用一个可以复用的模板替换己渲染元素。概念比较难理解,我们先看一个简单的示例来了解它的基本用法。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <div id ="app" > <child-component > <template scope ="props" > <p > 来自父组件的内容</p > <p > {{props.msg}}</p > </template > </child-component > </div > <script > Vue.component('child-component', { template: '\ <div class ="container" > \ <slot msg ="来自子组件的内容" > </slot > \ </div > ' }); var app = new Vue({ el: '#app' }) </script >
观察子组件的模板,在 元素上有一个类似 props 传递数据给组件的写法 msg= “xxx”,将数据传到了插槽。父组件中使用了 元素,而且拥有一个scope=”props ”的特性,这里的 props 只是一个临时变量,就像 v-for= ” item in items” 里面的item 一样。template 内可以通过临时变量 props 访问来自子组件插槽的数据 msg。
将上面的示例渲染后的结果为:
1 2 3 4 5 6 <div id ="app" > <div class ="container" > <p > 来自父组件的内容</p > <p > 来自子组件的内容</p > </div > </div >
作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项。示例代码如下:
访问 slot 组件高级用法 递归组件 内联模板 动态组件 异步组件 其他 $nextTick X-Templates 手动挂载实例 实战:两个常用组件的开发 本节以组件知识为基础,整合指令、事件等前面章节的内容,开发两个业务中常用的组件,即数字输入框和标签页。
开发一个数字输入框组件 数字输入框是对普通输入框的扩展,用来快捷输入一个标准的数字,如图 7-6 所示。
图 7-6 数字输入框
数字输入框只能输入数字,而且有两个快捷按钮,可以直接减l 或加1 。除此之外,还可以设置初始值、最大值、最小值,在数值改变时,触发一个自定义事件来通知父组件。
了解了基本需求后,我们先定义目录文件:
index.html 入口页
input-number.js 数字输入框组件
index.js 根实例
因为该示例是以交互功能为主,所以就不写css 美化样式了。
首先写入基本的结构代码,初始化项目。
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 数字输入框组件</title > </head > <body > <div id ="app" > </div > <script src ="https://cdn.staticfile.org/vue/2.2.2/vue.min.js" > </script > <script src ="input-number.js" > </script > <script src ="index.js" > </script > </body > </html >
index.js
1 2 3 var app = new Vue({ el : '#app' });
input-number.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Vue.component('input-number' , { template : '\ <div class="input-number">\ \ </div>' , props : { max : { type : Number , default : Infinity }, min : { type : Number , default : -Infinity }, value : { type : Number , default : 0 } } })
该示例的主角是 input-number.js,所有的组件配置都在这里面定义。先在 template 里定义了组件的根节点,因为是独立组件,所以应该对每个 prop 进行校验。这里根据需求有最大值、最小值、默认值(也就是绑定值) 3 个 prop, max 和 min 都是数字类型,默认值是正无限大和负无限大; value 也是数字类型, 默认值是0。
接下来,我们先在父组件引入 input-number 组件,并给它一个默认值 5 ,最大值 10 , 最小值 0 。
index.js
1 2 3 4 5 6 var app = new Vue({ el : '#app' , data : { value : 5 } });
index.html
1 2 3 <div id ="app" > <input-number v-model ="value" :max ="10" :min ="0" > </input-number > </div >
value 是一个关键的绑定值, 所以用了 v-model ,这样既优雅地实现了双向绑定,也让 API 看起来很合理。大多数的表单类组件都应该有一个 v-model , 比如输入框、单选框、多选框、下拉选择器等。
剩余的代码量就都聚焦到了 input-number.js 上。
我们之前介绍过, Vue 组件是单向数据流,所以无法从组件内部直接修改prop value 的值。
解决办法也介绍过, 就是给组件声明一个 data ,默认引用 value 的值,然后在组件内部维护这个 data :
1 2 3 4 5 6 7 8 Vue.component('input-number' , { data : function ( ) { return { currentValue : this .value } } })
这样只解决了初始化时引用父组件 value 的问题, 但是如果从父组件修改了 value , input-number 组件的 currentValue 也要一起更新。为了实现这个功能, 我们需要用到一个新的概念: 监听( watch )。
watch 选项用来监昕某个prop 或 data 的改变, 当它们发生变化时,就会触发 watch 配置的函数,从而完成我们的业务逻辑。在本例中,我们要监昕两个量: value 和 currentValue。 监听value 是要知晓从父组件修改了value ,监昕 currentValue 是为了当 currentValue 改变时,更新 value 。相关代码如下:
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 Vue.component('input-number' , { data : function ( ) { return { currentValue : this .value } }, watch : { currentValue : function (val ) { this .$emit('input' , val); this .$emit('on-change' , val); }, value : function (val ) { this .updateValue(val); } }, methods : { updateValue : function (val ) { if (val > this .max) val = this .max; if (val < this .min) val = this .min; this .currentValue = val; } }, mounted : function ( ) { this .updateValue(this .value); } })
从父组件传递过来的 value 有可能是不符合当前条件的(大于max, 或小于 min ),所以在选项 methods 里写了一个方法 updateValue , 用来过滤出一个正确的 currentValue。
watch 监听的数据的回调函数有 2 个参数可用, 第一个是新的值, 第二个是旧的值, 这里没有太复杂的逻辑, 就只用了第一个参数。在回调函数里, this 是指向当前组件实例的, 所以可以直接调用this. updateValue(), 因为 Vue 代理了 props 、data 、computed 及 methods。
监听 currentValue 的回调里, tthis.$emit(‘input’, val); 是在使用 v-model 时改变value 的;this.$emit(‘on-change’, val); 是触发自定义事件 on-change ,用于告知父组件数字输入框的值有所改变(示例中没有使用该事件)。
在生命周期 mounted 钩子里也调用了updateValue() 方法, 是因为第一次初始化时, 也对 value 进行了过滤。这里也有另一种写法, 在 data 选项返回对象前进行过滤:
1 2 3 4 5 6 7 8 9 10 11 Vue.component('input-number' , { data : function ( ) { var val = this .value; if (val > this .max) val = this .max; if (val < this .min) val = this .min; return { currentValue : this .value } } })
实现的效果是一样的。
最后剩余的就是补全模板 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 function isValueNumber (value ) { return (/(^-?[0-9]+\.{1}\d+$)|(^-?[1-9][0-9]*$)|(^-?0{1}$)/ ).test(value + '' ); } Vue.component('input-number' , { template : '\ <div class="input-number">\ <input type="text" :value="currentValue" @change="handleChange">\ <button @click="handleDown" :disabled="currentValue <= min">-</button>\ <button @click="handleUp" :disabled="currentValue >= max">+</button>\ </div>' , methods : { updateValue : function (val ) { if (val > this .max) val = this .max; if (val < this .min) val = this .min; this .currentValue = val; }, handleDown : function ( ) { if (this .currentValue <= this .min) return ; this .currentValue -= 1 ; }, handleUp : function ( ) { if (this .currentValue >= this .max) return ; this .currentValue += 1 ; }, handleChange : function (event ) { var val = event.target.value.trim(); var max = this .max; var min = this .min; if (isValueNumber(val)) { val = Number (val); this .currentValue = val; if (val > max) { this .currentValue = max; } else if (val < min) { this .currentValue = min; } } else { event.target.value = this .currentValue; } } } })
input 绑定了数据 currentValue 和原生的 change 事件, 在旬柄 handleChange 函数中,判断了当前输入的是否是数字。注意,这里绑定的 currentValue 也是单向数据流,并没有用 v-model ,所以在输入时, currentValue 的值并没有实时改变。如果输入的不是数字(比如英文和汉字等),就将输入的内容重置为之前的 currentValue 。如果输入的是符合要求的数字,就把输入的值赋给currentValue。
数字输入框组件的核心逻辑就是这些。回顾一下我们设计一个通用组件的思路,首先,在写代码前一定要明确需求,然后规划好 API 。一个 Vue 组件的 API 只来自 props 、events 和 slots ,确定好这 3 部分的命名、规则,剩下的逻辑即使第一版没有做好, 后续也可以迭代完善。但是 API 如果没有设计好,后续再改对使用者成本就很大了。
完整的示例代码如下:
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 数字输入框组件</title > </head > <body > <div id ="app" > <input-number v-model ="value" :max ="10" :min ="0" > </input-number > </div > <script src ="https://cdn.staticfile.org/vue/2.2.2/vue.min.js" > </script > <script src ="input-number.js" > </script > <script src ="index.js" > </script > </body > </html >
index.js
1 2 3 4 5 6 var app = new Vue({ el : '#app' , data : { value : 5 } });
input-number.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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 function isValueNumber (value ) { return (/(^-?[0-9]+\.{1}\d+$)|(^-?[1-9][0-9]*$)|(^-?0{1}$)/ ).test(value + '' ); } Vue.component('input-number' , { template : '\ <div class="input-number">\ <input type="text" :value="currentValue" @change="handleChange">\ <button @click="handleDown" :disabled="currentValue <= min">-</button>\ <button @click="handleUp" :disabled="currentValue >= max">+</button>\ </div>' , props : { max : { type : Number , default : Infinity }, min : { type : Number , default : -Infinity }, value : { type : Number , default : 0 } }, data : function ( ) { return { currentValue : this .value } }, watch : { currentValue : function (val ) { this .$emit('input' , val); this .$emit('on-change' , val); }, value : function (val ) { this .updateValue(val); } }, methods : { updateValue : function (val ) { if (val > this .max) val = this .max; if (val < this .min) val = this .min; this .currentValue = val; }, handleDown : function ( ) { if (this .currentValue <= this .min) return ; this .currentValue -= 1 ; }, handleUp : function ( ) { if (this .currentValue >= this .max) return ; this .currentValue += 1 ; }, handleChange : function (event ) { var val = event.target.value.trim(); var max = this .max; var min = this .min; if (isValueNumber(val)) { val = Number (val); this .currentValue = val; if (val > max) { this .currentValue = max; } else if (val < min) { this .currentValue = min; } } else { event.target.value = this .currentValue; } } }, mounted : function ( ) { this .updateValue(this .value); } })
练习1 :在输入框聚焦时,增加对键盘上下按键的支持,相当于加 1 和减 1。
练习2 :增加一个控制步伐的 prop-step,比如设置为 10 ,点击加号按钮, 一次增加 10 。
自定义指令 在第 5 章里我们已经介绍过了许多 Vue 内置的指令,比如 v-if、v-show 等,这些丰富的内置指令能满足我们的绝大部分业务需求,不过在需要一些特殊功能时,我们仍然希望对 DOM 进行底层的操作,这时就要用到自定义指令。
基本用法 自定义指令的注册方法和组件很像,也分全局注册和局部注册,比如注册一个 v-focus 的指令,用于在 、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Vue.directive('focus' , { }); var app = new Vue({ el : '#app' , directives : { focus : { } } })
写法与组件基本类似,只是方法名由 component 改为了 directive 。上例只是注册了自定义指令 v-focus ,还没有实现具体功能,下面具体介绍自定义指令的各个选项。
自定义指令的选项是由几个钩子函数组成的,每个都是可选的。
bind: 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作.
inserted : 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于document 中)
update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
componentUpdated : 被绑定元素所在模板完成一次更新周期时调用.
unbind : 只调用一次,指令与元素解绑时调用。
可以根据需求在不同的钩子函数内完成逻辑代码,例如上面的 v-focus ,我们希望在元素插入父节点时就调用,那用到的最好是 inserted 。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div id ="app" > <input type ="text" v-focus > </div > <script > Vue.directives('focus' , { inserted : function (el ) { el.focus(); } }); var app = new Vue({ el : '#app' }) </script >
在浏览器中的效果如图 8-1 所示。
图8- 1 v-focus 谊染后的效果
可以看到,打开这个页面, input 输入框就自动获得了焦点,成为可输入状态。
每个钩子函数都有几个参数可用,比如上面我们用到了el 。它们的含义如下:
el 指令所绑定的元素,可以用来直接操作DOM .
binding 一个对象,包含以下属性:
name 指令名,不包括v - 前缀。
value 指令的绑定值,例如 v-my-directive =”1 + 1”, value 的值是 2 .
oldValue 指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用.无论值是否改变都可用。
expression 绑定值的字符串形式。例如 v-my-directive= “1 + 1”, expression 的值是 “1 + 1”.
arg 传给指令的参数。例如 v-my-directive: foo, arg 的值是foo .
modifiers 一个包含修饰符的对象。例如 v-my-directive.foo.bar ,修饰符对象modifiers 的值是{ foo: true, bar: true }。
vnode Vue 编译生成的虚拟节点,在进阶篇中介绍.
oldVnode 上一个虚拟节点仅在update 和componentUpdated 钩子中可用。