计算属性是Vue.js中一个非常重要的概念,用于处理模板中的复杂逻辑。计算属性基于它们的依赖进行缓存,只有在相关依赖发生改变时才会重新求值。这使得计算属性在处理复杂计算和频繁更新的数据时,比方法调用更加高效。
计算属性的设计目标是:将复杂的逻辑从模板中抽离出来,使模板保持简洁和清晰,同时提高应用的性能。
考虑这样一个场景:我们需要在模板中显示一个反转的字符串。最简单的方式是在模板中使用表达式:
<div id="app">
<p>原始消息: {{ message }}</p>
<p>反转消息: {{ message.split('').reverse().join('') }}</p>
</div>
这种方式虽然可行,但存在几个问题:
模板中包含了复杂的JavaScript表达式,使得模板难以阅读和维护。
如果在模板中多次使用相同的表达式,每次都会重新计算,影响性能。
相同的逻辑在多个地方使用时代码重复,修改时需要同步修改多个地方。
每次渲染都会重新计算,即使依赖的数据没有变化。
计算属性解决了上述所有问题:
计算属性在Vue实例的computed选项中定义,可以像普通属性一样在模板中使用。
new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
firstName: '张',
lastName: '三'
},
computed: {
// 简单的计算属性
reversedMessage: function() {
return this.message.split('').reverse().join('');
},
// 基于多个依赖的计算属性
fullName: function() {
return this.firstName + ' ' + this.lastName;
},
// 也可以使用ES6箭头函数,但要注意this指向
// fullName: () => {
// // 箭头函数中的this不会指向Vue实例
// // 所以不推荐在计算属性中使用箭头函数
// }
}
});
<div id="app">
<p>原始消息: {{ message }}</p>
<p>反转消息: {{ reversedMessage }}</p>
<p>姓: {{ firstName }}</p>
<p>名: {{ lastName }}</p>
<p>全名: {{ fullName }}</p>
</div>
原始消息: {{ message }}
反转消息: {{ reversedMessage }}
全名: {{ fullName }}
在计算属性中不要使用箭头函数,因为箭头函数绑定了父级作用域的上下文,所以this不会指向Vue实例。
// 错误:箭头函数中的this不会指向Vue实例
computed: {
reversedMessage: () => {
return this.message.split('').reverse().join(''); // this是undefined或window
}
}
// 正确:使用普通函数
computed: {
reversedMessage: function() {
return this.message.split('').reverse().join('');
}
// 或者使用ES6方法简写
reversedMessage() {
return this.message.split('').reverse().join('');
}
}
计算属性最强大的特性之一是它的缓存机制。计算属性基于它们的响应式依赖进行缓存,只有在相关依赖发生改变时才会重新求值。
getReversedMessage() 调用
结果: {{ getReversedMessage() }}
每次渲染都会重新计算
reversedMessage 访问
结果: {{ reversedMessage }}
依赖不变时会使用缓存
观察结果:
计算函数被执行,结果被缓存
检查依赖是否变化,如果没变化,直接返回缓存值
标记缓存为"脏"(dirty),下次访问时重新计算
重新执行计算函数,更新缓存,返回新值
计算属性的缓存机制在以下场景特别有用:
计算属性和方法都可以用于处理模板中的逻辑,但它们有几个关键区别:
| 特性 | 计算属性 (computed) | 方法 (methods) |
|---|---|---|
| 缓存机制 | 有缓存,依赖不变时不会重新计算 | 无缓存,每次调用都会执行 |
| 调用方式 | 作为属性访问,不需要加括号 | 作为方法调用,需要加括号 |
| 响应式依赖 | 自动追踪依赖,依赖变化时重新计算 | 不自动追踪,每次调用都执行 |
| 语法 | {{ computedProperty }} |
{{ methodName() }} |
| 性能 | 更高(有缓存) | 较低(无缓存) |
| 适用场景 |
|
|
| 示例 |
|
|
// 适合使用计算属性的场景:
// 1. 基于现有数据计算新数据
computed: {
totalPrice() {
return this.price * this.quantity;
},
discountedPrice() {
return this.totalPrice * (1 - this.discount);
}
}
// 2. 格式化显示数据
computed: {
formattedDate() {
return new Date(this.timestamp).toLocaleDateString();
}
}
// 3. 过滤或筛选数组
computed: {
activeUsers() {
return this.users.filter(user => user.active);
}
}
// 适合使用方法的场景:
// 1. 事件处理
methods: {
handleClick() {
this.count++;
alert('按钮被点击!');
}
}
// 2. 每次都需要重新计算的逻辑
methods: {
getRandomNumber() {
return Math.random();
}
}
// 3. 需要参数的逻辑
methods: {
formatCurrency(amount) {
return '$' + amount.toFixed(2);
}
}
默认情况下,计算属性只有getter(只读),但你也可以提供一个setter,使计算属性可写。
计算属性的默认行为,当访问计算属性时调用。
computed: {
fullName: {
// getter
get: function() {
return this.firstName + ' ' + this.lastName;
}
}
}
当给计算属性赋值时调用,可以用于更新依赖的数据。
computed: {
fullName: {
// getter
get: function() {
return this.firstName + ' ' + this.lastName;
},
// setter
set: function(newValue) {
var names = newValue.split(' ');
this.firstName = names[0];
this.lastName = names[names.length - 1];
}
}
}
全名 (通过getter计算): {{ fullName }}
输入格式: "姓 名",例如 "李 四"
工作原理:
{{ fullName }}时,自动调用getter函数,返回计算值fullName赋值时,自动调用setter函数,分解姓名并更新firstName和lastNamesetter应该只用于更新计算属性所依赖的原始数据,而不应该执行复杂的业务逻辑或有其他副作用。如果有复杂的逻辑,应该放在方法中处理。
Vue提供了另一种观察和响应数据变化的方式:侦听器(watch)。虽然侦听器在某些场景下很有用,但通常计算属性是更好的选择。
| 特性 | 计算属性 (computed) | 侦听器 (watch) |
|---|---|---|
| 主要用途 | 基于依赖计算新值 | 观察数据变化并执行副作用 |
| 返回值 | 必须返回值 | 不需要返回值 |
| 缓存 | 有缓存 | 无缓存 |
| 异步支持 | 不支持异步操作 | 支持异步操作 |
| 代码组织 | 声明式,更简洁 | 命令式,更灵活 |
| 适用场景 |
|
|
| 示例 |
|
|
// 适合使用计算属性的场景:
// 1. 计算购物车总价
computed: {
totalPrice() {
return this.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
}
}
// 2. 过滤列表
computed: {
filteredItems() {
return this.items.filter(item => {
return item.name.includes(this.searchTerm);
});
}
}
// 适合使用侦听器的场景:
// 1. 数据变化时执行异步操作
watch: {
searchTerm: function(newVal) {
// 防抖处理
clearTimeout(this.debounce);
this.debounce = setTimeout(() => {
this.searchAPI(newVal);
}, 500);
}
}
// 2. 数据变化时执行复杂逻辑
watch: {
counter: function(newVal, oldVal) {
if (newVal > 10) {
alert('计数器超过10!');
}
}
}
// 3. 观察路由变化
watch: {
'$route': function(to, from) {
this.fetchData(to.params.id);
}
}
大多数情况下,应该优先使用计算属性,因为:
下面的演示展示了计算属性的多种实际应用场景,包括购物车计算、数据过滤、格式化等。
| 商品名称 | 单价 | 数量 | 小计 | 操作 |
|---|---|---|---|---|
| {{ item.name }} | {{ formatCurrency(item.price) }} | {{ item.quantity }} | {{ formatCurrency(item.price * item.quantity) }} |
商品总数: {{ totalItems }} 件
商品种类: {{ uniqueItems }} 种
商品总价: {{ formatCurrency(subtotal) }}
折扣金额: {{ formatCurrency(discountAmount) }}
运费: {{ formatCurrency(shipping) }}
优惠码示例: "SAVE10" 减10元
优惠券减免: {{ formatCurrency(couponDiscount) }}
商品总价: {{ formatCurrency(subtotal) }}
折扣金额: {{ formatCurrency(discountAmount) }}
过滤后商品: {{ filteredItems.length }} 件
应付总额: {{ formatCurrency(totalPrice) }}
computed: {
// 计算商品总价
subtotal() {
return this.items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
},
// 计算折扣金额
discountAmount() {
return this.subtotal * (this.discount / 100);
},
// 计算运费(根据商品总价)
shipping() {
if (this.subtotal === 0) return 0;
if (this.subtotal > 100) return 0; // 满100免运费
return 10; // 基础运费10元
},
// 计算优惠券折扣
couponDiscount() {
if (!this.useCoupon) return 0;
if (this.couponCode === 'SAVE10') return 10;
if (this.couponCode === 'SAVE20') return 20;
return 0;
},
// 计算应付总额
totalPrice() {
return this.subtotal - this.discountAmount + this.shipping - this.couponDiscount;
},
// 过滤商品(根据搜索词)
filteredItems() {
if (!this.searchTerm) return this.items;
return this.items.filter(item => {
return item.name.toLowerCase().includes(this.searchTerm.toLowerCase());
});
},
// 计算商品总数
totalItems() {
return this.items.reduce((total, item) => {
return total + item.quantity;
}, 0);
},
// 计算商品种类数
uniqueItems() {
return this.items.length;
}
}
现在你已经掌握了计算属性的使用,接下来我们将学习Vue的侦听器(watch),这是另一种响应数据变化的方式,特别适合处理异步操作和复杂副作用。