第3章计算属性和数据监听 在Vue组件的模板中,可以通过插值表达式输出模型变量的运算结果,例如以下代码中的{{a*b+b*c+a*c}}是插值表达式:

a={{a}},b={{b}},c={{c}}

插值表达式的取值:{{a*b+b*c+a*c}}

如果模板中嵌入了大量包含复杂运算的插值表达式,就会影响模板的可读性,而且插值表达式中不能包含流程控制逻辑,如不能包含ifelse条件判断或者for循环等。 为了弥补插值表达式的不足,Vue提供了以下3种替代方案。 (1) 用Vue的计算属性替代插值表达式。 (2) 用Vue的watch选项替代插值表达式。 (3) 用方法调用替代插值表达式。 本章会介绍这3种方式的具体用法,并且比较它们的优缺点。 3.1计算属性 Vue组件的computed选项是用来定义计算属性的。例程31演示了computed选项的基本用法。 例程31calculate.html

a={{a}},b={{b}},c={{c}}

插值表达式的取值:{{a*b+b*c+a*c}}

计算属性的取值:{{ result }}

在calculate.html中,{{ a*b+b*c+a*c }}和{{result}}能输出同样的结果。显然,{{result}}使模板更加简洁。result就是根组件的计算属性,它的定义方式如下: computed:{ result(){ //result计算属性的get函数 console.log('get result property') return this.a*this.b+this.b*this.c+this.a*this.c } } Vue框架会通过调用result()函数计算result的取值。这个result()函数相当于result计算属性的get函数。 通过浏览器访问calculate.html,会得到如图31所示的网页。{{ a*b+b*c+a*c }}和{{result}}的取值都是1100。 图31calculate.html的网页 3.1.1读写计算属性 在例程31中,result()函数用来读取result计算属性的值,它相当于result计算属性的get函数。在例程32中,为fullName计算属性同时提供了get函数和set函数。 例程32fullname.html

First name:

Last name:

Full name:

{{ fullName }}

通过浏览器访问fullname.html,会得到如图32所示的网页。 图32fullname.html的网页 为了跟踪fullName计算属性的get函数和set函数的调用时机,这两个函数都会向控制台输出一些日志。 如图33所示,如果修改网页上First name输入框或Last name输入框的值,会看到get函数被调用。如果修改Full name输入框的值,会看到set函数以及get函数先后被调用。 图33fullName计算属性的get和set函数的调用时机 firstName变量、lastName变量以及fullName计算属性之间存在依赖关系。从图33可以看出,fullName计算属性的get函数和set函数的作用如下: (1) 当firstName变量和lastName变量被更新,get函数会更新fullName计算属性。 (2) 当fullName计算属性被更新,set函数会更新firstName和lastName变量。 在fullName计算属性的set函数中,newValue参数表示更新后的fullName计算属性的值,this.fullName是更新前的值: set (newValue) { //set函数 console.log('call set') console.log('原先的fullName:'+this.fullName) var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } 3.1.2比较计算属性和方法 Vue组件的计算属性和方法都可以进行逻辑复杂的运算。在例程33中,result1计算属性和getResult1()方法能得到同样的运算结果,并且它们都依赖变量a; result2计算属性和getResult2()方法能得到同样的运算结果,并且它们都依赖变量b。 例程33compare.html

a={{a}},b={{b}}

a:

计算属性的取值:{{ result1 }}, {{ result2 }}

调用方法的结果:{{ getResult1() }}, {{getResult2() }}

通过浏览器访问compare.html,会得到如图34所示的网页。 图34compare.html的网页 为了跟踪result1计算属性和result2计算属性的get函数,以及getResult1()方法和getResult2()方法的调用时机,这些函数和方法都会向控制台输出一些日志。 图35更新变量a后的输出日志 在compare.html网页的输入框中修改变量a的值。如图35所示,从控制台输出的日志可以看出,Vue框架会调用result1计算属性的get函数,并且调用getResult1()方法和getResult2()方法。 Vue框架会把计算属性存放在专门的缓存中。由于result2计算属性不依赖变量a,因此当变量a被更新时,Vue框架无须调用result2计算属性的get函数,而是直接从缓存中读取result2的值,用来渲染DOM中的{{result2}}插值表达式。 由此可见,Vue框架对计算属性的get函数的调用做了性能优化。只有当计算属性依赖的变量被更新,Vue框架才会调用它的get函数。在本范例中,变量a被更新,Vue框架只需要调用resutl1计算属性的get函数,重新计算resutl1计算属性的值。 而对于getResult1()方法和getResult2()方法,当变量a被更新时,Vue框架无法知道上一次这两个方法的取值,因此在渲染DOM时,会分别调用这两个方法,重新计算{{getResult1()}}和{{getResult2()}}的取值。 3.1.3用计算属性过滤数组 在遍历数组时,如果希望只输出数组中符合特定条件的元素,那么可以用计算属性过滤数组。在例程34中,passStudents和unPassStudents是两个计算属性,它们对students数组变量进行了过滤。passStudents计算属性表示所有成绩及格的学生,unPassStudents计算属性表示所有成绩不及格的学生。 例程34array.html

及格同学

不及格同学

通过浏览器访问array.html,会得到如图36所示的网页。 图36array.html的网页 3.1.4计算属性实用范例:实现购物车 对于购物网站应用,需要在前端管理购物车的信息。购物车的信息中包含了选购的商品的名字、数量、单价和小计(数量×单价),还包含了所有选购商品的总金额。用户可以修改选购商品的数量,还可以删除选购的商品。 例程35实现了购物车。 例程35shoppingcart.html 购物车
序号 名称 价格 数量 小计 操作
{{index+1}} {{item.name}} {{ currency(item.price,2) }} {{ currency(item.count*item.price,2) }}
总金额: {{ currency( total,2 ) }}
cartItems数组变量表示用户选购的所有商品条目。在模板中通过v-for指令遍历cartItems数组变量,如: … cartItems数组中的每个商品条目表示该商品的具体购买信息,商品的具体购买信息包括: (1) 序号:{{index+1}}。 (2) 名称:{{item.name}}。 (3) 价格:{{ currency(item.price,2) }}。 (4) 数量:{{item.count}}。 (5) 小计:{{ currency(item.count*item.price,2) }}。 currency()方法用于对金额数字进行格式化,保留两位小数,并且会在数字开头加上货币符号¥。 在遍历了cartItems数组中的所有商品后,还会在网页上显示所有选购商品的总金额,如: 总金额: {{ currency( total,2 ) }} 其中,total是计算属性,它的定义如下: computed: { total(){ //计算属性total表示总金额 var sum=0; for(let i=0; i

{{ result }}

在mywatch.html中,Vue应用实例的watch选项中有一个num()函数,负责监听num变量。当num变量被更新,Vue的数据监听器就会调用这个num()函数。 通过浏览器访问mywatch.html,会得到如图38所示的网页。在网页的num变量的输入框输入新的数字,Vue的数据监听器就会调用num()函数,更新result变量的值。 图38mywatch.html的网页 提示: 除了通过watch选项监听特定数据,还可以调用$watch()方法监听数据。11.12.1节将演示$watch()方法的用法。 3.2.1用Web Worker执行数据监听中的异步操作 对于例程36,当用户在网页的num变量的输入框输入新的数字时,num()函数就会被Vue的数据监听器调用。num()函数会先调用sleep(2000)方法睡眠2s,通过这种睡眠的方式模拟耗时的操作。 num()函数是由浏览器的负责执行JavaScript脚本的主线程来执行的。当主线程执行sleep(2000)方法睡眠时,网页处于卡死状态,不能响应用户的任何操作。只有当主线程执行完num()函数,重新更新网页,网页才能继续响应用户的操作。 如果希望用户始终可以和网页进行顺畅地交互,不会出现网页卡死的情况,可以通过一个额外的线程异步执行耗时的操作。本节会利用HTML 5中的Web Worker线程执行耗时操作。 首先创建一个longtask.js文件(文件名可以任意选择),参见例程37。它的onmessage函数包含Worker线程接收到主线程发送的数据时执行的操作。 例程37longtask.js //睡眠函数,参数numberMillis是睡眠的ms数 function sleep(numberMillis) {…} //当Worker线程接收到主线程发送的数据时,调用此函数 onmessage=function(event){ var num=event.data //读取主线程发送过来的数据 sleep(2000) //睡眠2s,模拟耗时的操作 var result=Math.sqrt(num) //求平方根 postMessage(result) //向主线程发送运算结果 } 在onmessage函数中,event.data表示主线程发送过来的num变量。postMessage(result)用于向主线程发送result变量。 例程38会通过Worker线程执行耗时操作。 例程38mywatch-async.html

{{ result }}

在num()函数中,浏览器的主线程先通过以下语句为result变量赋予一个临时取值: this.result='正在运算,请稍后……' 接着,主线程通过new Worker('longtask.js')语句创建了Worker线程。然后执行以下语句注册用于监听接收数据的onmessage()函数: worker.onmessage=(event)=>this.result=event.data 当主线程接收到Worker线程发送的数据时,就会执行worker.onmessage()函数中的this.result=event.data语句,event.data表示Worker线程发送的数据。 接着,主线程向Worker线程发送newNum变量: worker.postMessage(newNum) 当笔者在创作此书时,发现不同浏览器对Web Worker的支持程度不一样。如果在Chrome浏览器中访问本地的mywatchasync.html,然后在网页的输入框中修改num变量的值,浏览器会产生以下错误: Uncaught (in promise) DOMException: Failed to construct 'Worker': Script at 'file:///C:/vue/sourcecode/chapter03/longtask.js' cannot be accessed from origin 'null'. 这是因为Chrome浏览器出于安全的原因,不允许使用本地的Web Worker线程。把该范例发布到JavaThinker.net网站,网址如下: www.javathinker.net/vue/mywatch-async.html 通过浏览器访问上述网址,就可以正常访问mywatchasync.html。在网页的输入框中修改num变量的值,网页不会卡死,主线程会先显示result变量的临时取值,参见图39。过2s后,主线程再显示由Worker线程运算得到的result变量。 图39网页显示result变量的临时取值 图310展示了主线程和Worker线程的通信以及交换数据的过程。 图310主线程和Worker线程的通信以及交换数据的过程 从图310可以看出,当主线程通过worker.postMessage(newNum)方法,向Worker线程发送newNum变量,就会触发Worker线程执行longtask.js中的onmessage()函数; 当Worker线程通过postmessage(result)方法,向主线程发送result变量,就会触发主线程执行worker.onmessage()函数。无论是主线程还是Worker线程,都可以通过event.data读取对方发送的数据。 3.2.2在watch选项中调用方法 在watch选项中还可以调用方法。例如,在例程39中,如果score变量被更新,Vue的数据监听器就会调用judge()方法。 例程39score.html

{{ result }}

对judge()方法做如下修改,使它通过JavaScript语言的setTimeout()函数执行异步操作: judge() { this.result="正在运算,请稍后......" setTimeout( ()=>{ if(this.score>=60) this.result='及格' else this.result='不及格' },2000) //延迟2s后再执行运算 } 以上judge()方法先给result变量赋予了一个临时值“正在运算,请稍后......”,然后利用setTimeout()函数设置了异步操作: 过2s后计算result变量的取值。judge()方法产生的运行效果是,网页上首先显示“正在运算,请稍后......”,过2s后再显示reuslt变量的实际取值。 3.2.3比较同步操作和异步操作 按照多个操作之间的执行顺序,可分为同步操作和异步操作。同步操作指一个操作执行完后,才能执行另一个操作。例如,对于组件的watch选项的num()函数: num(newNum, oldNum) { //num变量的监听函数 this.result="正在运算,请稍后......" //第1行 this.sleep(2000) //第2行 this.result=Math.sqrt(newNum) //第3行 } num()方法中的3行代码都是同步操作,主线程依次执行完以上3行代码,才能执行其他操作,例如依据当前的result变量重新渲染DOM。因此,尽管在num()函数中更新了两次result变量的值,但是在网页上只能看到最终更新后的result变量的值。 异步操作指多个操作可以各自独立运行。前端的异步操作有以下两种执行方式。 (1) 多线程执行方式。通过多个线程同时执行不同的异步操作。3.2.1节就是通过单独的Web Worker线程执行耗时的计算任务,而主线程会执行Vue框架的主流程。值得注意的是,早期的JavaScript版本并不支持多线程,因为这种运行方式会增加客户端的运行负荷,如果JavaScript脚本设计不合理,还会给客户机器带来安全隐患。 (2) 单线程执行方式。把多个异步操作放在一个异步队列中,由主线程以轮询的方式执行异步队列中的异步操作。 3.2.2节介绍的setTimeout()函数就是通过单线程方式执行异步操作。对于以下3.2.2节的judge()函数: judge() { this.result="正在运算,请稍后......" //第1行 setTimeout( ()=>{ //第2行 if(this.score>=60) this.result='及格' else this.result='不及格' },2000) //延迟2s后再执行运算 } judge()函数中的第1行和第2行代码是同步操作,但是setTimeout()函数中的第1个参数指定的函数包含一段异步操作,这段异步操作会放在异步队列中,主线程延迟2s后再执行这段异步操作。在这2s内,主线程可以执行Vue框架的其他操作,如渲染DOM,把网页上的{{result}}渲染为“正在运算,请稍后......”。2s后,主线程执行上述计算result变量实际取值的异步操作后,再重新渲染DOM,更新网页上的{{result}}。 3.2.4深度监听 默认情况下,当Vue的watch选项监听一个对象时,不会监听对象的属性的变化。如果希望监听对象的属性变化,可以在watch选项中把deep属性设为true,这样就能支持深度监听。 在例程310中,Vue的watch选项会监听student对象,由于deep属性设为true,因此当student.score属性被更新,watch选项中的handler()函数也会被执行。 例程310student.html

{{ result }}

通过浏览器访问student.html网页,在输入框中修改student.score属性的值,Vue的数据监听器会调用handler()函数,更新result变量。 当Vue的数据监听器深度监听一个对象时,不管对象的属性嵌套了多少层,只要属性发生变化,就会被监听。 3.2.5立即监听 通过浏览器访问例程310时,会看到网页上显示{{student.score}}的值为98,而不显示{{result}}的值。因为这时候Vue的数据监听器还没有监听到student.score属性的变化,因此不会调用watch选项中的handler()函数。 在Vue组件的生命周期中,如果希望在它的初始化阶段,Vue框架就会调用一次watch选项中的handler()函数,为result变量赋值,那么可以把watch选项的immediate属性设为true。 下面对student.html做如下修改,增加immediate: true语句: watch: { student: { handler(newStudent,oldStudent){…}, immediate: true, deep: true } } 再次通过浏览器访问student.html,会看到网页上{{student.score}}的初始值为98,{{result}}的初始值为“及格”。 3.2.6比较计算属性和数据监听watch选项 计算属性和数据监听watch选项可以完成一些相同的功能。例如当一个变量发生变化时,两者都能更新依赖这个变量的其他数据。 但是,计算属性和数据监听watch选项有不同的使用场合。watch选项擅长执行耗时的异步操作,3.2.1节、3.2.2节和3.3.3节已经对此做了介绍。而在只需要同步更新变量的场合,使用计算属性能使程序代码更加简洁,并且具有更好的运行性能。 例程311在watch选项中监听数据,它和例程32具有同样的功能,都能对firstName、lastName和fullName进行同步更新。 例程311fullnamewatch.html

First name:

Last name:

Full name:

{{ fullName }}

比较例程32和例程311,会发现有以下区别: (1) 例程32的代码更加简洁。computed选项中,只需要为fullName计算属性定义get和set函数。 (2) 例程311的代码更烦琐,需要在data选项中定义fullName变量,并且需要在watch选项中监听firstName、lastName和fullName这3个变量。 下面分别把例程32和例程311的模板中显示fullName信息的代码注释掉: 再通过浏览器分别访问例程32和例程311,修改网页上firstName变量输入框中的内容,从浏览器的控制台观察输出日志,会发现对于例程32中的fullName计算属性,由于在模板中不需要显示它,因此它的get函数不会被调用。 而对于例程311,只要firstName变量发生变化,watch选项中的firstName(newValue)函数就会被调用。 由此可见,计算属性比watch选项具有更好的运行性能。如果在页面上不需要显示和计算属性有关的数据,那么即使它依赖的变量发生变化,Vue框架也不会调用计算属性的get函数。 3.3Vue的响应式系统的基本原理 Vue框架能够对数据的更新快速做出响应。Vue 的响应式系统依赖以下3个重要的类: (1) Observer 类: 数据观察器,负责观察数据的更新。如果观察到数据更新,就把数据更新消息发送给Dep类。 (2) Dep类: 响应式系统的调度器,负责接收来自Observer类的数据更新消息,并把这个消息发送给相应的Watcher对象,Watcher对象是Watcher类的具体实例。 (3) Watcher 类: 数据监听器。负责接收来自Dep类的数据更新消息,执行相应的响应操作。 如图311所示,当用户更新了一个变量后,Observer类观察到这种更新,就会把更新消息发送给Dep类,Dep类再把更新消息发送给所有依赖这个变量的Watcher类,Watcher类会执行相应的操作对变量更新做出响应。 图311Vue的响应式系统 数据监听器Watcher类分为以下3种。 (1) 常规Watcher类(normalwatcher): 对于在组件的watch选项中监听的变量,使用normalwatcher。当被监听的变量发生变化,normalwatcher会立即执行watch选项中的相应函数。 (2) 计算属性Watcher类(computedwatcher): 对于在computed选项中定义的计算属性,使用computedwatcher。每个计算属性都对应一个computedwatcher对象。computedwatcher具有lazy(懒计算)特性: 假定计算属性b依赖变量a,当变量a更新时,computedwatcher不会立即重新计算b,而是只有当需要读取b时,才会重新计算b。3.2.6节也通过实验演示了这种lazy特性。 (3) 渲染Watcher类(renderwatcher): 每一个Vue组件都有相应的renderwatcher,当Vue组件的data选项中的变量或者computed选项中的计算属性发生变化时,renderwatcher就会重新渲染组件的DOM。 这3种 Watcher类有固定的执行顺序,按照先后顺序分别是normalwatcher、computedwatcher和renderwatcher。 3种Watcher类的执行顺序可以保证数据在业务逻辑上的一致性。例如,在例程312中,变量b依赖变量a,计算属性c依赖变量b,在watch选项中会监听变量a。在模板中会通过插值表达式{{a}}、{{b}}和{{c}}显示这3个变量的值。这3个变量的关系如下: b=a*10 c=b*10 例程312relation.html

{{a}},{{b}},{{c}}

如图312所示,在relation.html的网页上,修改变量a的输入框的值,会看到网页上变量b和计算属性c的值随之变化。 图312relation.html的网页 再观察浏览器的控制台的输出日志,会看到watch选项以及computed选项的函数先后输出以下日志: 开始监听a 结束监听a 计算c 由此可见,当变量a被更新后,Vue框架先通过normalwatcher计算变量b,再通过computedwatcher计算计算属性c,最后通过renderwatcher渲染DOM中的{{a}}、{{b}}和{{c}},这样就能保证变量a、变量b和计算属性c的数据一致性。 如果在watch选项的监听函数中需要读取计算属性,那么Vue框架会立即执行计算属性的get函数。下面对relation.html的computed选项和watch选项做如下修改: computed:{ c(){ console.log('计算c') return this.a*10 //计算属性c依赖变量a } }, watch:{ a(){ console.log('开始监听a') this.b=this.a*this.c //变量b依赖变量a和计算属性c console.log('结束监听a') } } 再次通过浏览器访问relation.html,修改网页中变量a的输入框的值,会看到浏览器的控制台输出以下日志: 开始监听a 计算c 结束监听a 由此可见,当Vue框架在执行变量a的监听函数时,在执行this.b=this.a*this.c语句之前,会先执行计算属性c的get函数。 3.4小结 本章主要介绍了Vue的computed选项和watch选项的用法。computed选项用来定义计算属性,它具有更好的运行性能,只有当计算属性依赖的变量发生变化,并且在需要读取计算属性的场合,才会执行它的get函数。watch选项用来监听变量,只要被监听的变量发生变化,就会立即执行相应的监听函数,这种监听函数可用来执行耗时的异步操作。 Vue的响应式系统的3个核心类是数据观察器Observer类、调度器Dep类和数据监听器Watcher类。Observer类负责观察数据的更新,Dep类负责调度相应的Watcher类,Watcher类负责执行相应的操作响应数据的更新。 Wacher类分为3种: normalwatcher、computedwatcher和renderwatcher。假定一个Vue组件的data选项中有一个变量a,computed选项中有一个计算属性b,计算属性b依赖变量a,watch选项会监听变量a。在组件的模板中有一个插值表达式{{a+b}}。当变量a发生变化时,3种Watcher类会依次完成以下操作: (1) normalwatcher执行watch选项中变量a的监听函数。 (2) computedwatcher重新计算变量b。 (3) renderwatcher重新渲染插值表达式{{a+b}}。 3.5思考题 1. 关于计算属性,以下说法正确的是()。(多选) A. 计算属性在computed选项中定义 B. 当计算属性依赖的变量被更新,Vue框架会调用计算属性的set函数 C. 计算属性在watch选项中定义 D. 当计算属性被更新,Vue框架会调用计算属性的set函数 2. 组件的()选项会通过normalwatcher监听数据。(单选) A. dataB. watchC. methodsD. computed 3. 以下是test.html的主要代码:

{{getB()}}

通过浏览器访问test.html,会出现()情况。(多选) A. 网页上显示{{getB()}}的值为100 B. 网页上显示{{getB()}}的值为1000 C. 网页上显示{{getB()}}的值为10 D. 浏览器的控制台显示执行this.b=this.a*100语句发生错误,错误原因为Write operation failed: computed property "b" is readonly 4. 以下是example.html的主要代码:

{{b}}

通过浏览器访问example.html,网页上显示{{b}}的值是()。(单选) A. {{b}}B. 100C. 1000D. 0 5. 对第4题的example.html中的watch选项做如下修改: watch:{ a:{ handler(){ this.b=this.a*10 }, immediate:true } } 通过浏览器访问example.html,网页上显示{{b}}的值是()。(单选) A. {{b}}B. 100C. 1000D. 0 6. 当组件的变量被更新时,组件的()选项适合设定用于立即响应变量更新的异步操作。(单选) A. dataB. methodsC. watchD. computed