Vue.js计算属性

计算属性是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('');
  }
}
                            

计算属性的缓存机制

计算属性最强大的特性之一是它的缓存机制。计算属性基于它们的响应式依赖进行缓存,只有在相关依赖发生改变时才会重新求值。

依赖数据变化
重新计算
更新缓存
{{ methodCallCount }}
方法调用次数
{{ computedCallCount }}
计算属性调用次数
{{ renderCount }}
渲染次数
缓存机制演示
使用方法

getReversedMessage() 调用

结果: {{ getReversedMessage() }}

每次渲染都会重新计算

使用计算属性

reversedMessage 访问

结果: {{ reversedMessage }}

依赖不变时会使用缓存

观察结果:

  • 点击"触发重新渲染"按钮,方法调用次数会增加,但计算属性调用次数不会增加(如果依赖没有变化)
  • 修改"消息"输入框,两者都会重新计算
  • 修改"数字"输入框,只有方法会重新计算(因为方法中没有使用number)
计算属性缓存的工作流程
1
首次访问计算属性

计算函数被执行,结果被缓存

2
后续访问计算属性

检查依赖是否变化,如果没变化,直接返回缓存值

3
依赖发生变化

标记缓存为"脏"(dirty),下次访问时重新计算

4
再次访问计算属性

重新执行计算函数,更新缓存,返回新值

缓存的实际意义

计算属性的缓存机制在以下场景特别有用:

  • 复杂计算:执行开销较大的计算(如大数据处理、复杂算法)
  • 频繁访问:在模板中多次使用相同的计算结果
  • 频繁渲染:在频繁更新的组件中避免重复计算
  • API数据转换:将从API获取的数据转换为视图需要的格式

计算属性 vs 方法

计算属性和方法都可以用于处理模板中的逻辑,但它们有几个关键区别:

特性 计算属性 (computed) 方法 (methods)
缓存机制 有缓存,依赖不变时不会重新计算 无缓存,每次调用都会执行
调用方式 作为属性访问,不需要加括号 作为方法调用,需要加括号
响应式依赖 自动追踪依赖,依赖变化时重新计算 不自动追踪,每次调用都执行
语法 {{ computedProperty }} {{ methodName() }}
性能 更高(有缓存) 较低(无缓存)
适用场景
  • 基于响应式数据的计算
  • 需要缓存结果的复杂计算
  • 在模板中多次使用的逻辑
  • 事件处理
  • 不需要缓存的计算
  • 每次调用都需要重新执行的逻辑
示例

computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName;
  }
}
// 使用: {{ fullName }}
                                            

methods: {
  getFullName() {
    return this.firstName + ' ' + this.lastName;
  }
}
// 使用: {{ getFullName() }}
                                            
何时使用计算属性 vs 方法

// 适合使用计算属性的场景:
// 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);
  }
}
                                

计算属性的setter和getter

默认情况下,计算属性只有getter(只读),但你也可以提供一个setter,使计算属性可写。

Getter(读取)

计算属性的默认行为,当访问计算属性时调用。


computed: {
  fullName: {
    // getter
    get: function() {
      return this.firstName + ' ' + this.lastName;
    }
  }
}
                                
Setter(写入)

当给计算属性赋值时调用,可以用于更新依赖的数据。


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];
    }
  }
}
                                
Setter和Getter演示

全名 (通过getter计算): {{ fullName }}

输入格式: "姓 名",例如 "李 四"

工作原理:

  • Getter: 当访问{{ fullName }}时,自动调用getter函数,返回计算值
  • Setter: 当给fullName赋值时,自动调用setter函数,分解姓名并更新firstNamelastName
注意:setter的副作用

setter应该只用于更新计算属性所依赖的原始数据,而不应该执行复杂的业务逻辑或有其他副作用。如果有复杂的逻辑,应该放在方法中处理。

计算属性 vs 侦听器

Vue提供了另一种观察和响应数据变化的方式:侦听器(watch)。虽然侦听器在某些场景下很有用,但通常计算属性是更好的选择。

特性 计算属性 (computed) 侦听器 (watch)
主要用途 基于依赖计算新值 观察数据变化并执行副作用
返回值 必须返回值 不需要返回值
缓存 有缓存 无缓存
异步支持 不支持异步操作 支持异步操作
代码组织 声明式,更简洁 命令式,更灵活
适用场景
  • 计算派生数据
  • 格式化显示数据
  • 简单数据转换
  • 执行异步操作
  • 数据变化时执行复杂逻辑
  • 需要观察特定数据的变化
示例

computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName;
  }
}
                                            

watch: {
  firstName: function(newVal, oldVal) {
    console.log('firstName从', oldVal, '变为', newVal);
    // 可以执行异步操作
    this.fetchData(newVal);
  }
}
                                            
何时使用计算属性 vs 侦听器

// 适合使用计算属性的场景:
// 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) }}

{{ discount }}%

折扣金额: {{ formatCurrency(discountAmount) }}

运费: {{ formatCurrency(shipping) }}


应付总额: {{ formatCurrency(totalPrice) }}

优惠码示例: "SAVE10" 减10元

优惠券减免: {{ formatCurrency(couponDiscount) }}

计算属性展示
subtotal

商品总价: {{ formatCurrency(subtotal) }}

discountAmount

折扣金额: {{ formatCurrency(discountAmount) }}

filteredItems

过滤后商品: {{ filteredItems.length }} 件

totalPrice

应付总额: {{ 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;
  }
}
                            

计算属性的最佳实践

应该做的
  • 保持计算属性纯粹:只进行计算,不要产生副作用
  • 使用描述性名称:名称应该清晰表达计算属性的用途
  • 避免复杂嵌套:如果一个计算属性过于复杂,考虑拆分成多个
  • 优先使用计算属性:而不是在模板中使用复杂表达式
  • 利用缓存:对于复杂或频繁使用的计算,使用计算属性
避免做的
  • 不要在计算属性中修改数据:计算属性应该是只读的
  • 避免异步操作:计算属性不支持异步,使用侦听器或方法
  • 不要过度使用setter:setter应只用于简单的数据更新
  • 避免过长的计算:长时间的计算会阻塞渲染
  • 不要依赖外部状态:计算属性应只依赖Vue实例的响应式数据
性能优化建议
  • 计算属性依赖最小化:只依赖真正需要的响应式数据
  • 避免不必要的计算:确保计算属性只在必要时重新计算
  • 使用计算属性替代方法:对于频繁使用的计算,使用计算属性利用缓存
  • 复杂计算考虑拆分:将复杂计算拆分成多个简单的计算属性
  • 使用计算属性进行数据格式化:而不是在模板或方法中处理

本章总结

  • 计算属性用于处理模板中的复杂逻辑,使模板保持简洁
  • 缓存机制是计算属性的核心特性,依赖不变时不重新计算
  • 计算属性 vs 方法:计算属性有缓存,作为属性访问;方法无缓存,需要调用
  • 计算属性 vs 侦听器:计算属性用于计算派生数据;侦听器用于执行副作用
  • setter和getter:计算属性默认只有getter,可以添加setter使其可写
  • 最佳实践:保持计算属性纯粹,使用描述性名称,避免复杂计算
  • 合理使用计算属性可以显著提高Vue应用的性能和可维护性

下一步:侦听器

现在你已经掌握了计算属性的使用,接下来我们将学习Vue的侦听器(watch),这是另一种响应数据变化的方式,特别适合处理异步操作和复杂副作用。