JavaScript的Vue.js库入门学习教程
vue是一个小巧轻便的javascript库。它有一个简单易懂的api,能够让开发者在开发web应用的时候更加简易便捷。实际上,一直让vue引以为豪的是它的便捷性、执行力、灵活性。
这篇教程的目的就是通过一些例子,让你能够概览一些基本的概念和特性。在接下来的其他教程里,你会学到vue更多的有用的特性,从而用vue搭建一个可扩展的项目。
mvvm 数据绑定
mvvm的本质是通过数据绑定链接view和model,让数据的变化自动映射为视图的更新。vue.js在数据绑定的api设计上借鉴了angular的指令机制:用户可以通过具有特殊前缀的html 属性来实现数据绑定,也可以使用常见的花括号模板插值,或是在表单元素上使用双向绑定:
<!-- 指令 --> <span v-text="msg"></span> <!-- 插值 --> <span>{{msg}}</span> <!-- 双向绑定 --> <input v-model="msg">
插值本质上也是指令,只是为了方便模板的书写。在模板的编译过程中,vue.js会为每一处需要动态更新的dom节点创建一个指令对象。每当一个指令对象观测的数据变化时,它便会对所绑定的目标节点执行相应的dom操作。基于指令的数据绑定使得具体的dom操作都被合理地封装在指令定义中,业务代码只需要涉及模板和对数据状态的操作即可,这使得应用的开发效率和可维护性都大大提升。
与angular不同的是,vue.js的api里并没有繁杂的module、controller、scope、factory、service等概念,一切都是以“viewmodel 实例”为基本单位:
<!-- 模板 --> <div id="app"> {{msg}} </div> // 原生对象即数据 var data = { msg: 'hello!' } // 创建一个 viewmodel 实例 var vm = new vue({ // 选择目标元素 el: '#app', // 提供初始数据 data: data })
渲染结果:
<div id="app"> hello! </div>
在渲染的同时,vue.js也已经完成了数据的动态绑定:此时如果改动data.msg的值,dom将自动更新。是不是非常简单易懂呢?除此之外,vue.js对自定义指令、过滤器的api也做了大幅的简化,如果你有angular的开发经验,上手会非常迅速。
数据观测的实现
vue.js的数据观测实现原理和angular有着本质的不同。了解angular的读者可能知道,angular的数据观测采用的是脏检查(dirty checking)机制。每一个指令都会有一个对应的用来观测数据的对象,叫做watcher;一个作用域中会有很多个watcher。每当界面需要更新时,angular会遍历当前作用域里的所有watcher,对它们一一求值,然后和之前保存的旧值进行比较。如果求值的结果变化了,就触发对应的更新,这个过程叫做digest cycle。脏检查有两个问题:
任何数据变动都意味着当前作用域的每一个watcher需要被重新求值,因此当watcher的数量庞大时,应用的性能就不可避免地受到影响,并且很难优化。
当数据变动时,框架并不能主动侦测到变化的发生,需要手动触发digest cycle才能触发相应的dom 更新。angular通过在dom事件处理函数中自动触发digest cycle部分规避了这个问题,但还是有很多情况需要用户手动进行触发。
vue.js采用的则是基于依赖收集的观测机制。从原理上来说,和老牌mvvm框架knockout是一样的。依赖收集的基本原理是:
将原生的数据改造成 “可观察对象”。一个可观察对象可以被取值,也可以被赋值。
在watcher的求值过程中,每一个被取值的可观察对象都会将当前的watcher注册为自己的一个订阅者,并成为当前watcher的一个依赖。
当一个被依赖的可观察对象被赋值时,它会通知所有订阅自己的watcher重新求值,并触发相应的更新。
依赖收集的优点在于可以精确、主动地追踪数据的变化,不存在上述提到的脏检查的两个问题。但传统的依赖收集实现,比如knockout,通常需要包裹原生数据来制造可观察对象,在取值和赋值时需要采用函数调用的形式,在进行数据操作时写法繁琐,不够直观;同时,对复杂嵌套结构的对象支持也不理想。
vue.js利用了es5的object.defineproperty方法,直接将原生数据对象的属性改造为getter和setter,在这两个函数内部实现依赖的收集和触发,而且完美支持嵌套的对象结构。对于数组,则通过包裹数组的可变方法(比如push)来监听数组的变化。这使得操作vue.js的数据和操作原生对象几乎没有差别[注:在添加/删除属性,或是修改数组特定位置元素时,需要调用特定的函数,如obj.$add(key, value)才能触发更新。这是受es5的语言特性所限。],数据操作的逻辑更为清晰流畅,和第三方数据同步方案的整合也更为方便。
组件系统
在大型的应用中,为了分工、复用和可维护性,我们不可避免地需要将应用抽象为多个相对独立的模块。在较为传统的开发模式中,我们只有在考虑复用时才会将某一部分做成组件;但实际上,应用类 ui 完全可以看作是全部由组件树构成的:
因此,在vue.js的设计中将组件作为一个核心概念。可以说,每一个vue.js应用都是围绕着组件来开发的。
注册一个vue.js组件十分简单:
vue.component('my-component', { // 模板 template: '<div>{{msg}} {{privatemsg}}</div>', // 接受参数 props: { msg: string<br> }, // 私有数据,需要在函数中返回以避免多个实例共享一个对象 data: function () { return { privatemsg: 'component!' } } })
注册之后即可在父组件模板中以自定义元素的形式调用一个子组件:
<my-component msg="hello"></my-component>
渲染结果:
<div>hello component!</div>
vue.js的组件可以理解为预先定义好了行为的viewmodel类。一个组件可以预定义很多选项,但最核心的是以下几个:
- 模板(template):模板声明了数据和最终展现给用户的dom之间的映射关系。
- 初始数据(data):一个组件的初始数据状态。对于可复用的组件来说,这通常是私有的状态。
- 接受的外部参数(props):组件之间通过参数来进行数据的传递和共享。参数默认是单向绑定(由上至下),但也可以显式地声明为双向绑定。
- 方法(methods):对数据的改动操作一般都在组件的方法内进行。可以通过v-on指令将用户输入事件和组件方法进行绑定。
- 生命周期钩子函数(lifecycle hooks):一个组件会触发多个生命周期钩子函数,比如created,attached,destroyed等等。在这些钩子函数中,我们可以封装一些自定义的逻辑。和传统的mvc相比,可以理解为 controller的逻辑被分散到了这些钩子函数中。
- 私有资源(assets):vue.js当中将用户自定义的指令、过滤器、组件等统称为资源。由于全局注册资源容易导致命名冲突,一个组件可以声明自己的私有资源。私有资源只有该组件和它的子组件可以调用。
- 除此之外,同一颗组件树之内的组件之间还可以通过内建的事件api来进行通信。vue.js提供了完善的定义、复用和嵌套组件的api,让开发者可以像搭积木一样用组件拼出整个应用的界面。这个思路的可行性在facebook开源的react当中也得到了印证。
基于构建工具的单文件组件格式
vue.js的核心库只提供基本的api,本身在如何组织应用的文件结构上并不做太多约束。但在构建大型应用时,推荐使用webpack+vue-loader这个组合以使针对组件的开发更高效。
webpack是由tobias koppers开发的一个开源前端模块构建工具。它的基本功能是将以模块格式书写的多个javascript文件打包成一个文件,同时支持commonjs和amd格式。但让它与众不同的是,它提供了强大的loader api来定义对不同文件格式的预处理逻辑,从而让我们可以将css、模板,甚至是自定义的文件格式当做javascript模块来使用。webpack 基于loader还可以实现大量高级功能,比如自动分块打包并按需加载、对图片资源引用的自动定位、根据图片大小决定是否用base64内联、开发时的模块热替换等等,可以说是目前前端构建领域最有竞争力的解决方案之一。
我在webpack的loader api基础上开发了vue-loader插件,从而让我们可以用这样的单文件格式 (*.vue) 来书写vue组件:
<style> .my-component h2 { color: red; } </style> <template> <div class="my-component"> <h2>hello from {{msg}}</h2> <other-component></other-component> </div> </template> <script> // 遵循 commonjs 模块格式 var othercomponent = require('./other-component') // 导出组件定义 module.exports = { data: function () { return { msg: 'vue-loader' } }, components: { 'other-component': othercomponent } } </script>
同时,还可以在*.vue文件中使用其他预处理器,只需要安装对应的webpack loader即可:
<style lang="stylus"> .my-component h2 color red </style> <template lang="jade"> div.my-component h2 hello from {{msg}} </template> <script lang="babel"> // 利用 babel 编译 es2015 export default { data () { return { msg: 'hello from babel!' } } } </script>
这样的组件格式,把一个组件的模板、样式、逻辑三要素整合在同一个文件中,即方便开发,也方便复用和维护。另外,vue.js本身支持对组件的异步加载,配合webpack的分块打包功能,可以极其轻松地实现组件的异步按需加载。
其他特性
vue.js还有几个值得一提的特性:
- 异步批量dom更新:当大量数据变动时,所有受到影响的watcher会被推送到一个队列中,并且每个watcher只会推进队列一次。这个队列会在进程的下一个 “tick” 异步执行。这个机制可以避免同一个数据多次变动产生的多余dom操作,也可以保证所有的dom写操作在一起执行,避免dom读写切换可能导致的layout。
- 动画系统:vue.js提供了简单却强大的动画系统,当一个元素的可见性变化时,用户不仅可以很简单地定义对应的css transition或animation效果,还可以利用丰富的javascript钩子函数进行更底层的动画处理。
- 可扩展性:除了自定义指令、过滤器和组件,vue.js还提供了灵活的mixin机制,让用户可以在多个组件中复用共同的特性。
利用new vue()创建一个vue实例
我们可以先初始化一个html页面,然后我们需要引入vue 的 js 文件。引入的方式有很多,我们可以在script中引入vue的cdn,或者去官网上下载vue的min.js,或者用 npm 安装一个vue的依赖。方便起见,本文中就用cdn引入。
<!doctype html> <html> <head> <title>从零开始学vue</title> </head> <body> <script src="http://cdn.jsdelivr.net/vue/1.0.16/vue.js"></script> </body> </html>
当你在开发过程中,确保你使用的是没有压缩过的版本,因为没有压缩的版本会提供有用的详细的警告,将会给你的代码书写节省很多时间。
我们先在body里面写入一个div,并且创建一个vue实例,然后将实例和div绑定起来。
当你创建一个新的vue实例的时候要使用vue()构造器,然后在你的实例中指出你的挂载点。这个挂载点就是你想要划分出来的vue实例的边界。挂载点和实例边界是一一对应的,你只能在挂载点上处理你实例边界内的事务,而不能在你的挂载点上处理实例边界外的事务。
在vue实例中设置挂载点的参数是 “ el ”,el 的值可以用dom元素定义。
<!doctype html> <html> <head> <title>从零开始学vue</title> </head> <body> <div id="vueinstance">这中间就是实例挂载点的实例边界</div> <script src="http://cdn.jsdelivr.net/vue/1.0.16/vue.js"></script> <script> // 创建一个新的vue实例,并设置挂载点 var v = new vue({ el : '#vueinstance' }); </script> </body> </html>
就像你在上面看到的那样,new一个vue()就能创建一个新的实例,然后指定一个dom元素作为实例的挂载点。定义挂载点的时候,我们用到了css选择器的id来定义。实例化的名字也可以自己来定义,以便之后调用。
利用v-model进行双向数据绑定
我们可以用v-model对input输入框进行绑定,从而我们可以使用动态的获取数据对象的值。你可以认为v-model是一个指定的属性,就像html元素的属性。这里的双向数据绑定可以用在很多表单元素上,比如input、textarea、select。
vue利用v-model这个指令绑定了一个数据,而这个数据是我们希望能够通过用户输入操作而更新的数据。
比如我们下面这个例子,我们要在input标签上绑定数据name,我们需要在vue实例中的data对象中实现声明。
<div id="vueinstance"> 输入您的姓名: <input type="text" v-model="name"> </div>
<script src="http://cdn.jsdelivr.net/vue/1.0.16/vue.js"></script>//之后这行会省略 <script> var v = new vue({ el : '#vueinstance', data : { name : '_appian' } }); </script>
无论用户输入多少次,这个name都会被自动更新。并且,如果name的值被改变了,其他有映射name的地方的值也会被修改。
这种input框和映射的同步修改的原因,就是利用v-model这个指令,让数据通过底层的数据流进行绑定后直接修改。这就是数据的双向绑定的概念。
为了证明这个概念,我们可以利用$data打印出数据的映射来看看。
<div id="vueinstance"> 输入您的姓名: <input type="text" v-model="name"> <p>{{ $data | json }}</p> //#1 <p>{{ name }}</p> //#2 </div>
<script> var v = new vue({ el : '#vueinstance', data : { name : '_appian' } }); </script>
其1:
$data是vue实例观察的数据对象,本质类型是一个对象,所以可以转成json。可以用一个新的对象替换。实例代理了它的数据对象的属性。
{{}},利用两个花括号进行插值。这里插入的值是$data实时变化的值。
| json,只是一个更直观的能让数据展示出来的方法。也可以看做是一个过滤器,就像json.stringify()的效果一样。
其2:
{{ name }},就是直接在插值表达式,两个花括号中间插入数据变量,直接映射name的值。
vue就是这么简单的进行数据的双向绑定,只需要一个v-model指令就可以,而不需要利用js或者jq来控制数据。相信你能从上面的例子中理清逻辑。
利用v-on进行事件绑定
vue是利用v-on指令进行事件监听和事件分发的。你可以在vue的实例中创建一个方法来绑定监听事件,可以创建一个方法来分派一个点击事件。
下面的例子中,我们将创建一个say方法,这个方法绑定在一个button上。点击产生的效果就是弹出一个带有用户name的欢迎框。为了将这个方法指派给button,我们需要用v-on:click来进行事件绑定。
<div id="vueinstance"> 输入您的姓名: <input type="text" v-model="name"> <button v-on:click="say">欢迎点击</button> //#1 <button @click="say">欢迎点击</button> //#2 </div>
<script> var v = new vue({ el : '#vueinstance', data : { name : '_appian' }, methods : { say : function(){ alert('欢迎' + this.name); } } }); </script>
当然了,不仅是可以绑定click点击事件,还可以绑定其他鼠标事件,键盘输入事件等一些js的事件类型。比如v-on:mouseover,v-on:keydown, v-on:submit, v-on:keypress,v-on:keyup.13等等,或者是一些其他的自定义事件。
在开发过程中,你可能会频繁的用到事件绑定,v-on写起来有点麻烦,所以上例中提供了两种写法,#2就是对#1写法的缩写。利用@代替v-on,这里不多说。
利用v-if或者v-show进行条件判定
如果我们希望用户在登录之后才能看到欢迎弹窗,而如果没有登录则给它一个登录界面。vue会提供给我们v-if指令和v-show指令用来控制不同登录状态下的显示内容。
利用先前的例子,我们可以用loginstatus的值来控制是否登录,如果是true则显示输入框和按钮让他能够看到欢迎弹窗,但如果是false(即未登录),则只能看到输入账号、密码的输入框和提交按钮(暂时不进行身份验证,只改变登录状态)。
<div id="vueinstance"> //loginstatus为true时会显示的section <section v-if="loginstatus"> 输入您的姓名: <input type="text" v-model="name"> <button v-on:click="say">欢迎点击</button> <button @click="switchloginstatus">退出登录</button> </section> //loginstatus为false时会显示的section <section v-if="!loginstatus"> 登录用户: <input type="text"> 登录密码: <input type="password"> <button @click="switchloginstatus">登录</button> </section> </div>
<script> var v = new vue({ el : '#vueinstance', data : { name : '_appian', loginstatus : false }, methods : { say : function(){ alert('欢迎' + this.name); }, switchloginstatus : function(){ this.loginstatus = !this.loginstatus; } } }); </script>
this的执行就是实例v。this的指向是一个需要自己去搞懂的问题,这里不多说。
在上述例子中,只要把v-if换成v-show,一样可以获得等同的效果。同时v-if和v-show他们都支持v-else,但是绑定v-else命令的标签的前一兄弟元素必须有 v-if 或 v-show。
在上面的例子中,只要点击“登录”或者“退出登录”按钮都会触发switchloginstatus方法,只要触发了这个方法就会导致loginstatus的状态变化(在true和false中进行切换),从而改变了html中的v-if的判断条件结果的变化,基于当前的loginstatus的布尔值的状态,使得显示的section是不同状态下的section。
v-show和v-if之间有什么区别呢?
在切换 v-if 块时,vue有一个局部编译/卸载过程,因为 v-if 之中的模板也可能包括数据绑定或子组件。v-if 是真实的条件渲染,因为它会确保条件块在切换当中合适地销毁与重建条件块内的事件监听器和子组件。
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——在条件第一次变为真时才开始局部编译(编译会被缓存起来)。
相比之下,v-show 简单得多——元素始终被编译并保留,只是简单地基于 css 切换。
一般来说,v-if 有更高的切换消耗而 v-show 有更高的初始渲染消耗。因此,如果需要频繁切换 v-show 较好,如果在运行时条件不大可能改变 v-if 较好。
这个差别也许对你目前的开发来说并不重要,但是你还是要注意和留心,因为当你的项目开发变大的时候,这点会变得重要起来。
利用v-for输出列表
如果你是经营一个电商平台的商人的话,你一定有很多页面都需要渲染商品列表的输出。v-for指令允许循环我们的数组对象,用 “element in arrayobj” 的方式,念作“循环arrayobj这个数据对象里的每一个element”。
下面的例子中,我们将会利用v-for指令循环输出一个商品列表。每一个商品都会在一个li中,li中输出商品的名称、价格和商品类型。
<div id="vueinstance"> <ul> <li v-for="el in products"> {{ el.name }} - ¥ {{ el. price }} - {{ el. category }} </li> </ul> </div>
<script> var v = new vue({ el : '#vueinstance', data : { products : [ {name: 'microphone', price: 25, category: 'electronics'}, {name: 'laptop case', price: 15, category: 'accessories'}, {name: 'screen cleaner', price: 17, category: 'accessories'}, {name: 'laptop charger', price: 70, category: 'electronics'}, {name: 'mouse', price: 40, category: 'electronics'}, {name: 'earphones', price: 20, category: 'electronics'}, {name: 'monitor', price: 120, category: 'electronics'} ] } }); </script>
当然了,data中的数组对象,可以不用像上面这样定义也可以,我们可以从数据库导入,或者是利用ajax请求得到。这里只是为了演示v-for。
有时候我们可能会需要拿到商品在数组对象里的对应下标。我们可以用$index来获得。
//#1 <li v-for="el in products"> {{ $index }} - {{ el.name }} - ¥ {{ el. price }} - {{ el. category }} </li> //#2 <li v-for="(index,el) in products"> {{ index }} - {{ el.name }} - ¥ {{ el. price }} - {{ el. category }} </li>
计算属性computed
计算属性的应用场景,一般是在有一个变量的值需要其他变量计算得到的时候,会用到。
比如,例如用户在输入框输入一个数 x,则自动返回给用户这个数的平方 x²。你需要对输入框进行数据绑定,然后当数据修改的时候,就马上计算它的平方。
<div id="vueinstance"> 输入一个数字: <input type="text" v-model="value"> <p>计算结果:{{ square }}</p> </div>
<script> var v = new vue({ el : '#vueinstance', data : { value : 1 }, computed : { square : function(){ return this.value * this.value; } } }); </script>
计算属性定义数值是通过定义一系列的function,就像我们先前定义methods对象的时候是一样的。比如square方法是用来计算变量“square”的,其方法的返回值是两个this.value的乘积。
接下来可以用computed做一个复杂一点例子。系统会随机取一个1~10以内的数字,用户可以在输入框随机输入一个1~10之内的数字,如果刚好用户的输入和系统的随机数刚好匹配,则游戏成功,反之失败。
<div id="vueinstance"> 输入1~10内的数字: <input type="text" v-model="value"> <p>计算结果:{{ resultmsg }}</p> </div>
<script> var v = new vue({ el : '#vueinstance', data : { value : null, randnum : 5//第一次随机数为5 }, methods : { getrandnum: function(min, max){ return math.floor(math.random() * (max - min + 1)) + min; } }, computed : { resultmsg : function(){ if (this.value == this.randnum) { this.randnum = this.getrandnum(1, 10); return '你猜对了!'; } else { this.randnum = this.getrandnum(1, 10); return '猜错了,再来!'; } } } }); </script>
后记
到此为止,你就已经能够掌握了vue的基本使用,世界上最简洁最漂亮的框架之一,它的构建有着自己完整的设计思想,而且越来越流行。这个框架足够小而轻,在你的开发中会给你带来更加流畅的用户体验,并有效提高开发效率。上文中举了一系列例子,都掌握了吗?
上一篇: 神秘的五号病床