本章概要
- 计算属性的定义
- 计算属性的缓存
- v-for和v-if替代方案
在模板中使用表达式非常方便,但如果表达式的逻辑过于复杂,模板就会变得臃肿不堪且难以维护。例如:
<div id="app">
<p>{{ message.split('').reverse.join('') }}</p>
</div>
Mustache 语法中的表达式调用了 3 个方法来最终实现字符串的反转,逻辑过于复杂,如果在模板中还要多次引用此处的翻转字符串,就更加难以处理了,这时就应该使用计算属性。
6.1 计算属性的定义
计算属性是以函数形式,在选项对象的 computed 选项中定义。将上述字符串翻转的功能使用计算属性实现。如下:
例6-1
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>计算属性</title>
</head>
<body>
<div id="app">
<p>原始字符串: {{ message }}</p>
<p>计算后的反转字符串: {{ reversedMessage }}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
message: '独行月球'
}
},
computed: {
// 计算属性的 getter
reversedMessage(){
return this.message.split('').reverse().join('');
}
}
}).mount('#app');
</script>
</body>
</html>
声明了一个计算属性 reversedMessage ,可以像普通属性一样将数据绑定到模板中的计算属性。
渲染结果如下:
![image.png](https://img-blog.csdnimg.cn/img_convert/3bd9a7fb66bbcd1d53f602f9d7f13e54.png#clientId=uece0b4ac-8378-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=114&id=u4de9326f&margin=[object Object]&name=image.png&originHeight=114&originWidth=435&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5393&status=done&style=none&taskId=ue7cb9db9-09a3-4e82-b4b6-f488012eb60&title=&width=435)
当 message 属性的值改变时,reversedMessage 的值也会自动更新,并且会自动同步更新 DOM 部分。在浏览器的 Console 窗口中修改 vm.message 的值,可以发现 reversedMessage 的值也会随之改变。
计算属性默认只有 getter,因此是不能直接修改计算属性的,如果需要,则可以提供一个 setter 。如下:
例6-2
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>计算属性的getter和setter</title>
</head>
<body>
<div id="app">
<p>First name: <input type="text" v-model="firstName"></p>
<p>Last name: <input type="text" v-model="lastName"></p>
<p>{{ fullName }}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
firstName: 'Smith',
lastName: "Will"
}
},
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
let names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
}).mount('#app');
</script>
</body>
</html>
任意修改 firstName 或 lastName 的值,fullName 的值也会自动更新,这是调用它的 getter() 函数实现的。在浏览器的 Console 窗口输入 vm.fullName = “zhang san”,可以看到 firstName 和 lastName 的值也同时发生了改变,这是调用 fullName 的 setter 函数实现的。
6.2 计算属性的缓存
复杂的表达式也可以放到方法中实现,然后在绑定表达式中调用方法即可。以下代码使用方法实现了字符串的翻转,最终结果和 例6-1 使用计算属性相同。如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>计算属性</title>
</head>
<body>
<div id="app">
<p>原始字符串: {{ message }}</p>
<p>方法调用后的反转字符串: {{ reversedMessage() }}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
message: '我爱吃番茄'
}
},
methods: {
reversedMessage() {
return this.message.split('').reverse().join('');
}
}
}).mount('#app');
</script>
</body>
</html>
计算属性是基于它的响应式依赖进行缓存的,只有在计算属性的相关响应式依赖发生改变时才会更新值。
这就意味着只要message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不会再次执行函数;而如果采用方法,那么不管什么时候访问 reversedMessage() ,该方法都会被调用。
为了对计算属性和方法的区别有更直观的认识。以下代码同事使用方法和计算属性,如下:
例6-4
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>计算属性</title>
</head>
<body>
<div id="app">
<p>原始字符串: {{ message }}</p>
<p>计算后的反转字符串: {{ reversedMessage }}</p>
<p>方法调用后的反转字符串: {{ reversedMessage2() }}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
message: '我爱吃西红柿'
}
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
alert("计算属性");
return this.message.split('').reverse().join('');
}
},
methods: {
reversedMessage2: function () {
alert("方法");
return this.message.split('').reverse().join('');
}
}
}).mount('#app');
let msg = vm.reversedMessage;
msg = vm.reversedMessage2();
</script>
</body>
</html>
在计算属性 reversedMessage 的 getter() 函数和 reversedMessage2() 方法中调用 alert() 语句显示一个消息框,在根组件实例构建后,分别访问 vm.reversedMessage 计算属性和调用 vm.reversedMessage2() 方法。
使用浏览器打开该页面,可以依次看到“计算属性”“方法”“方法”共三个消息框,前两个消息框是模板中的 Mustache 标签被替换时显示的,最后一个“方法”消息框是代码最后调用vm.reversedMessage2() 方法显示的,可以看到最后对 vm.reversedMessage 计算属性的访问并没有弹出消息框,这是因为它所依赖的 message 属性并未发生改变。
下面代码中的计算属性 now 在初次渲染后不会再更新,因为 Date.now() 不是响应式依赖
computed: {
now:function(){
return Date.now();
}
}
那么为什么需要缓存呢?
假设有一个性能开销比较大的计算属性 A ,它需要遍历一个巨大的数组并做大量的计算,然后可能还有其它的计算属性依赖于 A。如果没有缓存,将不可避免地多次执行 A 的 getter。
如果业务实现不希望有缓存,那么可以使用方法来代替。
6.3 v-for和v-if替代方案
使用计算属性替代 v-for 和 v-if 一起使用的情况。如下:
例6-5
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>v-for与计算属性</title>
</head>
<body>
<div id="app">
<h1>已完成的工作计划</h1>
<ul>
<li v-for="plan in completedPlans">
{{plan.content}}
</li>
</ul>
<h1>未完成的工作计划</h1>
<ul>
<li v-for="plan in incompletePlans">
{{plan.content}}
</li>
</ul>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
plans: [
{content: '狗了个狗', isComplete: false},
{content: '买菜', isComplete: true},
{content: '写PPT', isComplete: false},
{content: '做饭', isComplete: true},
{content: '打羽毛球', isComplete: false}
]
}
},
computed: {
completedPlans() {
return this.plans.filter(plan => plan.isComplete);
},
incompletePlans(){
return this.plans.filter(plan => !plan.isComplete);
}
}
}).mount('#app');
</script>
</body>
</html>
Vue.js 的作者不建议将 v-for和 v-if 一起使用,这是因为即使由于 v-if 指令的作用只渲染了部分元素,但在每次重新渲染的时候仍要遍历整个列表,而不论渲染的元素内容是否发生了改变。
采用计算属性过滤后再遍历,可以获得以下好处:
- 过滤后的列表只会在 plans 数组发生相关变化时才被重新计算,过滤更高效
- 使用 v-for=“plan in completedPlans” 之后,在渲染的时候只遍历已完成的计划,渲染更高效
- 解耦渲染的逻辑,可维护性(对逻辑的更改和扩展)更强