Vue计算&侦听属性:深入解析与实践
2.4 计算属性
2.4.1 插值语法实现反转字符串
在Vue开发中,我们常常会遇到需要对数据进行处理并展示在页面上的需求。以反转字符串为例,假设我们希望用户输入一个字符串,然后将其反转后展示在页面中。使用插值语法来实现这一功能,代码如下:
<body><!-- 需求:用户输入字符串,然后反转展示在页面中 --><div id="app"><h1>{{msg}}</h1>输入的信息:<input type="text" v-model="info" /><hr><!--直接写在插值语法中,有以下三个问题:1. 可读性差。2. 代码没有得到复用。3. 难以维护。-->反转的信息:{{info.split('').reverse().join('')}}<hr>反转的信息:{{info.split('').reverse().join('')}}<hr>反转的信息:{{info.split('').reverse().join('')}}<hr></div><script>const vm = new Vue({el: "#app",data: {msg: "插值语法 - 反转字符串案例",info: ""}});</script>
</body>
在这段代码中,我们直接在插值语法 {{}}
内使用JavaScript表达式 info.split('').reverse().join('')
来实现字符串的反转。然而,这种方式存在明显的弊端。首先,代码的可读性较差,因为插值语法内的表达式逻辑较为复杂,不便于阅读和理解。其次,当需要在多个地方展示反转后的字符串时,相同的表达式需要重复书写,代码没有得到复用。最后,一旦逻辑发生变化,例如反转算法需要调整,就需要在所有使用该表达式的地方进行修改,维护成本较高。正如Vue官网所说:模版内的表达式以及指令语法非常便利,但设计它们的初衷是用于简单运算,在模版中放入太多的逻辑会让模版过重且难以维护。
2.4.2 方法实现反转字符串
为了解决插值语法的问题,我们可以将字符串反转的逻辑封装到一个方法中。代码如下:
<body><div id="app"><h1>{{msg}}</h1>输入的信息:<input type="text" v-model="info" /><br><!-- 在插值语法中可以调用方法,小括号不能省略。这个方法需要是Vue实例所管理的。 -->反转的信息:{{reverseInfo()}}<br><!-- 缺点:效率有问题 会重复调用 -->反转的信息:{{reverseInfo()}}<br>反转的信息:{{reverseInfo()}}<br>反转的信息:{{reverseInfo()}}<br></div><script>const vm = new Vue({el: "#app",data: {msg: "计算属性 - 反转字符串案例",info: ""},methods: {// 反转信息的方法reverseInfo() {return this.info.split("").reverse().join("");}}});</script>
</body>
在上述代码中,我们在Vue实例的 methods
选项中定义了 reverseInfo
方法,该方法实现了字符串反转的功能。在模板中,通过 {{reverseInfo()}}
调用该方法来展示反转后的字符串。这种方式虽然解决了代码复用和一定程度的维护性问题,但在效率上存在不足。当页面中有多个地方调用 reverseInfo()
方法时,每次调用都会重新执行该方法,即使 info
的值并未发生改变。如果 reverseInfo
方法内部的逻辑较为复杂,多次重复执行会消耗较多的性能资源。
2.4.3 计算属性实现反转字符串
1. 什么是计算属性?
计算属性是Vue提供的一种强大特性,它允许我们使用Vue的原有属性(通常是 data
对象当中的属性),经过一系列的运算或计算,最终得到一个全新的属性。这个全新的属性有自己独立的属性名和属性值,并且与 data
中的原始属性相关联,但又具有相对独立的存在意义。例如,我们可以基于 data
中的一个字符串属性,通过特定的运算得到一个反转后的字符串属性,这个反转后的字符串属性就是一个计算属性。
2. 计算属性的作用?
- 代码复用:将复杂的数据处理逻辑封装在计算属性中,避免在模板中重复书写相同的表达式,提高了代码的复用性。例如在反转字符串的案例中,使用计算属性后,无论在模板的多少个地方需要展示反转后的字符串,都只需引用计算属性即可。
- 便于维护:当数据处理逻辑发生变化时,只需在计算属性的定义处进行修改,而无需在模板中多处查找和修改相同的表达式,降低了维护成本。例如,如果我们要改变字符串反转的算法,只需要修改计算属性中的逻辑,模板中的引用无需变动。
- 执行效率高:计算属性具有缓存机制,只有当它所依赖的
data
中的属性值发生变化时,才会重新计算。如果多次访问计算属性且其依赖的属性值未改变,计算属性不会重复计算,而是直接返回之前缓存的结果,从而提高了执行效率。这在复杂计算场景下优势尤为明显。
3. 计算属性怎么用?
计算属性需要使用新的配置项 computed
。其语法格式如下:
computed: {// 这是一个计算属性,可以有多个计算属性 计算属性1: {// 当读取计算属性1的值的时候或该计算属性所关联的Vue原有属性的值发生变化时,getter方法被自动调用。get() { },// 当修改计算属性1的值的时候,setter方法被自动调用。set(val) { }},计算属性2: {}
}
需要注意以下几点:
- 不能在
set
方法里面改原有属性的值:在set
方法中修改计算属性所依赖的原始属性值,可能会导致无限循环等问题。因为计算属性的值是基于原始属性计算得出的,修改原始属性又会触发计算属性的重新计算,从而陷入死循环。 - 计算属性通过
vm.$data
,vm._data
是无法访问的:计算属性是Vue实例基于data
中的属性计算生成的,它并不直接存储在vm.$data
或vm._data
中,而是通过computed
配置项进行管理和访问。 - 计算属性的
getter/setter
方法中的this
是vm
:在getter
和setter
方法内部,this
指向Vue实例本身,这使得我们可以方便地访问data
中的属性和其他方法。例如在getter
方法中可以通过this.info
访问data
中的info
属性来进行计算。 - 计算属性的
getter
方法的调用时机:- 第一个时机:初次访问该属性。当模板中首次出现对计算属性的引用时,
getter
方法会被调用,计算并返回属性值。 - 第二个时机:计算属性所依赖的数据发生变化时。例如,某个计算属性依赖于
data
中的info
属性,当info
的值发生改变时,该计算属性的getter
方法会再次被调用,重新计算属性值,以保证数据的一致性。
- 第一个时机:初次访问该属性。当模板中首次出现对计算属性的引用时,
- 计算属性的
setter
方法的调用时机:当计算属性被修改时。(在setter
方法中通常是修改属性,因为只有当属性值变化时,计算属性的值就会联动更新。注意:计算属性的值被修改并不会联动更新属性的值。)例如,在模板中有一个双向绑定到计算属性的输入框,当用户在输入框中修改值时,会触发计算属性的setter
方法。 - 计算属性没有真正的值,每一次都是依赖
data
属性计算出来的:计算属性本身并不存储实际的值,它的值是根据其依赖的data
属性动态计算得出的。这也是为什么当依赖的data
属性变化时,计算属性会重新计算。 - 计算属性的
getter
和setter
方法不能使用箭头函数,因为箭头函数的this
不是vm
,而是window
:由于箭头函数没有自己独立的this
,它的this
是继承自外层作用域,在Vue的计算属性中使用箭头函数会导致this
指向错误,无法正确访问Vue实例的data
和methods
。
下面是一个完整的使用计算属性实现字符串反转的示例:
<body><div id="app"><h1>{{msg}}</h1>输入的信息:<input type="text" v-model="info" /><br>反转的信息:{{reversedInfo}}<br>反转的信息: <input type="text" v-model="reversedInfo" /><!-- 多次调用计算属性,只会执行一次,效率高 -->{{fun}}<br><!-- {{fun}}<br>{{fun}}<br>{{fun}}<br>{{fun}}<br> --><!-- 多次调用方法,每次都是执行,效率低 --><!-- {{hello()}}<br>{{hello()}}<br>{{hello()}}<br>{{hello()}}<br>{{hello()}}<br> --></div><script>const vm = new Vue({el: "#app",data: {msg: "计算属性 - 反转字符串案例",info: ""},methods: {hello() {console.log("hello方法执行了");return "hello";}},computed: {// 可以定义多个计算属性fun: {// get方法的调用时机get() {console.log("getter方法调用了");//console.log(this === vm)return "haha" + this.info;},// 不能使用箭头函数,使用箭头函数会导致this的指向是:window// get:()=>{// console.log('getter方法调用了')// console.log(this === vm)// return 'haha'// },set(val) {console.log("setter方法调用了");//console.log(this === vm)}},// 完整写法/* reversedInfo : { get(){return this.info.split('').reverse().join('')},// 当修改计算属性的时候,set方法被自动调用。set(val){//console.log('setter方法被调用了。')// 不能这么更改计算属性的值,这样做就递归了。//this.reversedInfo = val// 怎么修改计算属性呢?原理:计算属性的值变还是不变,取决于计算属性关联的Vue原始属性的值。// 也就是说:reversedInfo变还是不变,取决于info属性的值变不变。// 将值赋给info,通过info来实现反推修改,//例如,需要将翻转值修改为hello,则需要反转给info,通过info再反转回来// 本质上:修改计算属性,实际上就是通过修改Vue的原始属性来实现的。this.info = val.split('').reverse().join('')}} */// 简写形式:set不需要的时候。reversedInfo() {return this.info.split("").reverse().join("");}}});</script>
</body>
在这个示例中,我们定义了 reversedInfo
计算属性。其中,reversedInfo
的 getter
方法实现了字符串反转的逻辑,当 info
的值发生变化时,reversedInfo
会自动重新计算。并且我们还展示了计算属性的缓存特性,多次调用 fun
计算属性时,只要其依赖的 info
未改变,getter
方法不会重复执行。
4、计算属性的简写形式
当我们只需要考虑读取计算属性的值,而不涉及修改操作时,可以启用计算属性的简写形式。在这种简写形式中,计算属性后面跟的是一个方法,该方法返回 getter
被调用后的值。示例代码如下:
computed: {reversedInfo() {console.log('getter被调用了');return this.info.split('').reverse().join('')}
}
在这个例子中,reversedInfo
就是一个使用简写形式的计算属性。它等同于完整写法中只定义了 getter
方法的情况,省略了 setter
方法的定义,因为在该场景下不需要对计算属性进行赋值操作。
2.5 侦听属性
2.5.1 侦听属性作用
侦听属性的核心作用是监视某个属性的变化。当被监视的属性一旦发生改变时,就会执行预先定义好的某段代码。与计算属性相比,侦听属性可以实现异步流程,这是它的一个重要特点。计算属性主要用于根据已有数据进行同步计算并返回结果,而侦听属性更侧重于在属性值变化时触发特定的行为,这些行为可以是同步的,也可以是异步的,并且不需要像计算属性那样依赖 return
语句来返回值。
2.5.2 watch配置项
在Vue中,监视属性变化需要使用 watch
配置项。通过 watch
配置项,我们可以监视多个属性,监视哪个属性,就把该属性的名字拿过来即可。具体有以下几种情况:
- 可以监视Vue的原有属性:例如,我们可以监视
data
中定义的普通属性,像number
属性,当number
的值发生变化时,触发相应的处理逻辑。 - 如果监视的属性具有多级结构,一定要添加单引号:当监视的属性是对象的嵌套属性,如
a.b
,必须使用单引号将属性路径括起来,即'a.b'
。这样Vue才能正确识别要监视的具体属性。 - 无法直接监视对象深层次属性:如果要监视的对象中某个深层次属性,如
a.b
,而b
属性在对象初始化时压根不存在,那么直接监视b
是无效的。在这种情况下,需要特殊处理,例如使用深度监视来确保能够捕获到深层次属性的变化。 - 启用深度监视,默认是不开启深度监视的:深度监视用于监视对象或数组的嵌套属性的变化。默认情况下,Vue的
watch
配置项不会自动监视对象内部深层次属性的变化,只有当对象或数组的引用发生改变时才会触发监视。如果需要监视对象内部深层次属性的变化,就需要手动启用深度监视。 - 也可以监视计算属性:计算属性的值是基于其他属性计算得出的,我们同样可以通过
watch
配置项来监视计算属性的变化,当计算属性的值发生改变时,执行相应的逻辑。
2.5.3 如何深度监视:
- 监视多级结构中某个属性的变化:写法是
'a.b.c' : {}
。注意这里要使用单引号将属性路径括起来。例如,如果有一个对象obj
,其结构为obj = {a: {b: {c: 0}}}
,要监视c
属性的变化,就可以在watch
配置项中使用'a.b.c' : {}
的形式,并在对象中定义相应的处理逻辑。 - 监视多级结构中所有属性的变化:可以通过添加深度监视来完成,即在
watch
配置项中设置deep : true
。例如,对于对象obj = {a: {b: 0, c: 0}, d: 0}
,如果要监视obj
内部所有属性(包括a
对象中的b
和c
属性)的变化,就可以这样配置:
watch: {obj: {deep: true,handler(newValue, oldValue) {console.log("对象内部属性发生了变化");}}
}
2.5.4 如何后期添加监视:
- 调用API:可以通过调用
vm.$watch('number1', {})
来后期添加对某个属性的监视。其中,'number1'
是要监视的属性名,后面的对象可以配置监视的相关选项,如immediate
(是否在初始化时立即调用handler
方法)、deep
(是否启用深度监视)以及handler
(属性变化时执行的处理函数)等。
2.5.5 watch的简写:
- 简写的前提:当不需要配置
immediate
和deep
时,可以使用简写形式。 - 如何简写:在
watch
配置项中,直接使用属性名作为键,属性值为一个函数,该函数接收两个参数newVal
(新值)和oldVal
(旧值)。例如:
watch: {number1(newVal, oldVal) {console.log(newVal, oldVal);}
}
- 后期添加的监视如何简写:对于后期添加的监视,可以使用
vm.$watch('number1', function(newVal, oldVal){})
的形式进行简写。其中,'number1'
是要监视的属性名,后面的函数是属性变化时执行的处理函数,接收newVal
和oldVal
两个参数。
以下是一个完整展示 watch
配置项使用的示例代码:
<body><div id="app"><h1>{{msg}}</h1>数字:<input type="text" v-model="number"><br>数字:<input type="text" v-model="a.b"><br>数字:<input type="text" v-model="a.c"><br>数字:<input type="text" v-model="a.d.e.f"><br>数字(后期添加监视):<input type="text" v-model="number2"><br></div><script>const vm = new Vue({el: "#app",data: {msg: "侦听属性的变化",// 原有属性number: 0,// 多层次属性a: {b: 0,c: 0,d: {e: {f: 0}}},number2: 0},// 计算属性computed: {hehe() {return "haha" + this.number;}},watch: {//1、可以监视多个属性,监视哪个属性,请把这个属性的名字拿过来即可。//1.1 可以监视Vue的原有属性/* number : {// 初始化的时候,调用一次handler方法。immediate : true,// 这里有一个固定写死的方法,方法名必须叫做:handler// handler方法什么时候被调用呢?当被监视的属性发生变化的时候,handler就会自动调用一次。// handler方法上有两个参数:第一个参数newValue,第二个参数是oldValue// newValue是属性值改变之后的新值。// oldValue是属性值改变之前的旧值。handler(newValue, oldValue){console.log(newValue, oldValue)// this是当前的Vue实例。// 如果该函数是箭头函数,这个this是window对象。不建议使用箭头函数。console.log(this)}}, *///1.2 如果监视的属性具有多级结构,一定要添加单引号:'a.b'/* 'a.b' : { handler(newValue, oldValue){console.log('@')} },'a.c' : { handler(newValue, oldValue){console.log('@')} }, */// 无法监视b属性,因为b属性压根不存在。/* b : { handler(newValue, oldValue){console.log('@')} } *///1.3 启用深度监视,默认是不开启深度监视的。a: {// 什么时候开启深度监视:当你需要监视一个具有多级结构的属性,并且监视所有的属性,需要启用深度监视。deep: true,handler(newValue, oldValue) {console.log("@");}},//1.4 也可以监视计算属性/* hehe : {handler(a, b){console.log(a, b)}} *///2、 监视某个属性的时候,也有简写形式,什么时候启用简写形式?// 当只有handler回调函数的时候,可以使用简写形式。number(newValue, oldValue) {console.log(newValue, oldValue);}}});//3、 如何后期添加监视?调用Vue相关的API即可。{}要么是对象,要么是一个回调。//3.1 语法:vm.$watch('被监视的属性名', {})/* vm.$watch('number2', {immediate : true,deep : true,handler(newValue, oldValue){console.log(newValue, oldValue)}}) *///3.2 这是后期添加监视的简写形式。vm.$watch("number2", function (newValue, oldValue) {console.log(newValue, oldValue);});</script>
</body>
2.5.6 computed和watch如何选择?
在实际开发中,computed
和 watch
都可以用于响应数据的变化,但它们的使用场景有所不同。以下通过一个比较大小的案例来详细说明如何选择。
2.5.6.1 watch实现
<body><div id="app"><h1>{{msg}}</h1>数值1:<input type="number" v-model="num1"><br>数值2:<input type="number" v-model="num2"><br>比较大小:{{compareResult}}</div><script>const vm = new Vue({el: "#app",data: {msg: "比较大小的案例",num1: 0,num2: 0,compareResult: ""},watch: {// 监视num1num1: {immediate: true,handler(val) {let result = val - this.num2;if (result == 0) {this.compareResult = val + " = " + this.num2;} else if (result > 0) {this.compareResult = val + " > " + this.num2;} else {this.compareResult = val + " < " + this.num2;}}},// 监视num2num2: {immediate: true,handler(val) {let result = this.num1 - val;if (result == 0) {this.compareResult = this.num1 + " = " + val;} else if (result > 0) {this.compareResult = this.num1 + " > " + val;} else {this.compareResult = this.num1 + " < " + val;}}}}});</script>
</body>
在这个示例中,我们使用 watch
分别监视 num1
和 num2
的变化。当 num1
或 num2
的值发生改变时,handler
函数会被调用,通过比较两个数值的大小,更新 compareResult
的值,从而在页面上显示比较结果。
2.5.6.2 computed实现
<body><div id="app"><h1>{{msg}}</h1>数值1:<input type="number" v-model="num1"><br>数值2:<input type="number" v-model="num2"><br>比较大小:{{compareResult}}</div><script>const vm = new Vue({el: "#app",data: {msg: "比较大小的案例",num1: 0,num2: 0},computed: {// 计算属性的简写形式compareResult() {let result = this.num1 - this.num2;if (result == 0) {return this.num1 + " = " + this.num2;} else if (result > 0) {return this.num1 + " > " + this.num2;} else {return this.num1 + " < " + this.num2;}}}});</script>
</body>
这里使用 computed
来实现相同的比较大小功能。compareResult
计算属性根据 num1
和 num2
的值进行比较,并返回相应的比较结果。当 num1
或 num2
的值发生变化时,compareResult
会自动重新计算,从而更新页面上的显示。
2.5.6.3 总结
- 优先选择computed:以上比较大小的案例可以用
computed
完成,并且比watch
还要简单。所以在实际开发中,要遵守一个原则:如果computed
和watch
都能够完成的功能,优先选择computed
。这是因为computed
具有缓存机制,只有当它所依赖的属性值发生变化时才会重新计算,而watch
则是在属性值变化时就会执行相应的处理函数,可能会导致不必要的性能开销。 - 选择watch的情况:如果要开启异步任务,只能选择
watch
。因为computed
依靠return
语句来返回计算结果,它是同步的,无法直接处理异步操作。而watch
不需要依赖return
,可以在handler
函数中方便地执行异步任务,如使用setTimeout
进行延迟操作、发起AJAX请求等。例如,在比较大小的案例中,如果需要延迟3s出现结果,就可以在watch
的handler
函数中使用setTimeout
来实现:
watch: {// 监视num1num1: {immediate: true,handler(val) {let result = val - this.num2;// 需求2:3s后出现比较结果// 此时使用箭头函数,箭头函数没有this,会向上找到num1,num1是vm的属性,// 如果将此时箭头函数转成普通函数,this就会是windowsetTimeout(() => {if (result == 0) {this.compareResult = val + " = " + this.num2;} else if (result > 0) {this.compareResult = val + " > " + this.num2;} else {this.compareResult = val + " < " + this.num2;}}, 3000);}},// 监视num2num2: {immediate: true,handler(val) {let result = this.num1 - val;setTimeout(() => {if (result == 0) {this.compareResult = this.num1 + " = " + val;} else if (result > 0) {this.compareResult = this.num1 + " > " + val;} else {this.compareResult = this.num1 + " < " + val;}}, 3000);}}
}
而如果在 computed
中使用 setTimeout
,由于 setTimeout
是异步的,它的 this
是 window
,并且 computed
不能直接返回异步操作的结果,所以无法实现延迟显示比较结果的需求。
2.5.7 关于函数的写法,写普通函数还是箭头函数?
在Vue开发中,关于函数的写法,无论是普通函数还是箭头函数,目标都是为了让 this
和 vm
相等,以便能够正确访问Vue实例的属性和方法。具体来说:
- 所有Vue管理的函数,建议写成普通函数:例如在
methods
中定义的方法、computed
的getter
和setter
方法、watch
的handler
函数等,这些函数都是由Vue进行管理和调用的,使用普通函数可以确保this
指向Vue实例vm
。 - 所有不属于Vue管理的函数,例如
setTimeout
的回调函数、Promise
的回调函数、AJAX
的回调函数,建议使用箭头函数:这些函数通常是在JavaScript的原生环境中执行,使用箭头函数可以避免this
指向的问题。因为箭头函数没有自己独立的this
,它的this
是继承自外层作用域,在这种情况下,外层作用域通常是Vue实例,所以可以正确访问Vue实例的属性和方法。例如在watch
中使用setTimeout
时,使用箭头函数作为setTimeout
的回调函数,可以确保在回调函数中能够正确访问Vue实例的属性和方法,如this.compareResult
。