1.计算属性
1.1 为什么引入计算属性
vue是不建议在模板插值表达式中写太多逻辑计算的代码的,因为这会让模板变得不清晰、臃肿、难以看出你想表达什么逻辑。
例如如下代码:
<p>{{ username.splite('.')[0].toUpperCase() }}</p>
表达式先用'.'进行了数组拆分,然后将拆分的数组0号元素进行了大写转换。其实我最终的目的是想得到用户的姓氏的大写,这种复杂的插值表达式就会增大我们阅读代码的难度。
其次当我们的页面中如果不止出现了一次插值表达式的计算,而这些计算又是相同的,那么vue会进行很多次相同的逻辑计算,即影响性能又使代码看起来啰嗦。
因此我们需要对复杂的插值表达式使用模板语法进行封装,简化我们的代码。
1.2 使用方式
在createApp方法中传入computed属性,在该属性对象中声明计算属性对象,用例如下:
<html>
<header>
</header>
<body>
<div id="root">
<p>{{ fullInfo }}</p>
<p>{{ fullInfo }}</p>
<p>{{ fullInfo }}</p>
<p>{{ getFullInfo() }}</p>
<p>{{ getFullInfo() }}</p>
<p>{{ getFullInfo() }}</p>
</div>
</body>
<script src="./js/vue.global.js"></script>
<script>
let vue = Vue.createApp({
data() {
return {
name: '码小飞飞飞飞',
age: 27
}
},
methods: {
getFullInfo() {
console.log('普通方法被调用');
return this.name + "_" + this.age;
}
},
computed: {
//声明一个key为fullInfo的计算属性
fullInfo() {
console.log('计算属性被调用');
//返回计算属性的值
return this.name + "_" + this.age;
}
}
});
vue.mount('#root')
</script>
</html>
在上述例子中,我们创建了一个名为fullInfo的计算属性,并在页面上渲染了三次该属性。通过控制台打印可以看出,计算属性fullInfo对应的方法只被调用了一次,而getFullInfo方法则被调用了三次,因此可以证明通过计算属性能够对数据进行缓存,即相同的输入属性值(参与计算属性运算的data中的属性)的计算属性只会被运算一次,以后再次运算的结果会从缓存中直接获取。
上述例子是计算属性的简写形式,fullInfo方法是计算属性的get方法的简写形式,其实计算属性还可以声明set方法,用来对新计算的属性值进行处理。
<!--该写法不被推荐-->
<html>
<header>
</header>
<body>
<div id="root">
<p>{{ newFullInfo }}</p>
<button @click="age++;">点我改变age</button>
<br/>
<button @click="newFullInfo = '72-码小菜'">点我改变newFullInfo</button>
</div>
</body>
<script src="./js/vue.global.js"></script>
<script>
let vue = Vue.createApp({
data() {
return {
name: '码小飞飞飞飞',
age: 27
}
},
computed: {
//计算属性的完整写法
newFullInfo: {
get() {
console.log('newFullInfo计算属性被调用');
return this.age + "-" + this.name
},
set(newValue) {
console.log(newValue);
//解构赋值
[this.age, this.name] = newValue.split("-");
}
}
}
});
vue.mount('#root')
</script>
</html>
在上述例子中,我们声明了一个名为newFullInfo的计算属性,并完整声明了其get和set方法。计算属性的set方法是在直接修改计算属性本身时才会被调用的,当修改参与计算属性计算的属性时,set方法并不会被调用,通过点击例子中的两个button按钮即可证明。
虽然计算属性是可以被直接修改,但是官方并不建议这么做,我们应该尽量避免这种写法,或者说这种写法是没什么意义的。
因为计算属性仅仅是一个由我们已经存在的属性计算出来的快照值,当我们直接修改计算属性后如果不使用set方法进行转换,那么快照值是不会映射到我们的真实属性上的。而我们一旦显式声明了set方法进行了转换,那么计算属性的get方法又会被重新调用(因为参与计算的真实属性又发生了变化),相当于计算属性被重复赋值了两次。这不但对性能造成了浪费,又做了无意义的操作,还会显得我们的代码不简洁,达到的效果还完全一样。因此官方是不建议这么做的。
1.3 适用场景
计算属性适合的业务场景有两个:
- 当我们的插值表达式非常复杂,难以直观看出表达式的明确意图时,可以使用计算属性来使我们的业务逻辑变得清晰。
- 当我们的页面有非常多的重复插值表达式时,可以使用计算属性来提高性能,因为计算属性会帮我们缓存计算结果。
tips:计算属性的动态更新也是使用了ES6的Proxy代理对象来实现的。代理对象实现了对data属性的set方法的监听。感兴趣的小伙伴可以深入研究一下。
2.侦听器
侦听器是用来侦听我们的响应式属性变化的,当我们的业务需要对某些响应式属性的更新进行实时页面反馈时可以使用侦听器实现。例如vue官网的搜索栏,就可以使用侦听器来实现。
2.1 侦听器的基本使用方式
侦听器的关键字为watch,以对象的形式出现。在对象内可以声明要侦听的响应式属性,然后声明一个侦听的处理方法,方法传入两个值,第一个值为属性变更后的值,第二个值为属性变更前的值。用例如下:
<html>
<header>
</header>
<body>
<div id="root">
<p>{{ name }}</p>
<button @click="this.name = 'lisi'">点我改变name</button>
</div>
</body>
<script src="./js/vue.global.js"></script>
<script>
let vue = Vue.createApp({
data() {
return {
name: '码小飞飞飞飞'
}
},
watch: {
//监控响应式属性name,方法第一个属性为新值,第二个属性为旧值
name(newName, oldName) {
console.log(newName, oldName);
}
}
});
vue.mount('#root')
</script>
</html>
点击button后,可以看到watch中监控name属性的方法被调用了。
我们还可以使用路径描述来实现监听,如下面的例子:
<html>
<header>
</header>
<body>
<div id="root">
<p>{{ name }}</p>
<button @click="this.name = 'lisi'">点我改变name</button>
<p>{{ properties.age }}</p>
<button @click="properties.age++;">点我改变age</button>
</div>
</body>
<script src="./js/vue.global.js"></script>
<script>
let vue = Vue.createApp({
data() {
return {
name: '码小飞飞飞飞',
properties: {
age: 27
}
}
},
watch: {
//普通属性监听
'name'(newName, oldName) {
console.log(newName, oldName);
},
//带路径分隔符的监听
'properties.age'(newAge, oldAge) {
console.log(newAge, oldAge);
}
}
});
vue.mount('#root')
</script>
</html>
注意:路径监听的key只能使用路径,不能使用表达式,比如三目运算符xxx > xxx ? 'propertyA' : 'propertyB' 是不支持的
2.2 侦听器的相关属性
上一小节的例子使用的侦听器写法是简化写法,当我们想进行一些额外的设置时需要使用完整写法。完整写法需要将被监视的属性从一个方法变为一个对象,对象中可以设置handler方法来对应原来简化写法的方法,完整写法用例会在本节中的其他小章节中进行展示。
2.2.1 深度监听
watch 默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果我们需要监听深层属性,需要设置deep属性为true
<html>
<header>
</header>
<body>
<div id="root">
<p>{{ properties.age }}</p>
<button @click="properties.age++;">点我改变age</button>
</div>
</body>
<script src="./js/vue.global.js"></script>
<script>
let vue = Vue.createApp({
data() {
return {
properties: {
age: 27
}
}
},
watch: {
'properties': {
handler(newValue, oldValue) {
console.log(newValue, oldValue);
},
// 当deep属性被注释时,点击button改变properties中的age属性时,此监听不会生效
deep: true
}
}
});
vue.mount('#root')
</script>
</html>
2.2.2 即时回调
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。如果我们需要在侦听器创建时就执行侦听回调,则需要设置immediate为true
<html>
<header>
</header>
<body>
<div id="root">
<p>{{ properties.age }}</p>
<button @click="properties.age++;">点我改变age</button>
</div>
</body>
<script src="./js/vue.global.js"></script>
<script>
let vue = Vue.createApp({
data() {
return {
properties: {
age: 27
}
}
},
watch: {
'properties': {
handler(newValue, oldValue) {
console.log(newValue, oldValue);
},
deep: true,
//设置immediate为true,则此侦听器创建时就会触发handler函数
immediate: true
}
}
});
vue.mount('#root')
</script>
</html>
2.2.3 回调触发时机
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项
3.计算属性与侦听器的异同
计算属性可以看作是简单的侦听器实现,换句话说就是计算属性能完成的工作,侦听器也可以完成。但是侦听器能完成的工作,计算属性则不一定能完成。
例如当我们想在属性变化时进行一些异步操作,比如网络请求、任务延迟一定时间后再设置属性等场景时,计算属性就不能满足要求,此时一定要使用侦听器来完成,这种需要异步的场景被VUE官方称作“副作用”。
但是计算属性也有其优势,就是计算属性会自带缓存功能,可以优化我们的页面性能。且有些场景下使用计算属性完成功能比使用侦听器完成功能的代码量小很多,使我们的代码较为简洁。