计算属性

模板内的表达式非常便利,但它们的设计初衷是用于简单运算。在模板中放入太多的逻辑会让模板过重且难以维护。对于任何复杂逻辑,都应该使用计算属性。

为什么需要计算属性

假设我们要显示一个字符串的反转:

<div id="app">
  <p>原始字符串: {{ message }}</p>
  <p>反转字符串: {{ message.split('').reverse().join('') }}</p>
</div>

模板变得臃肿,难以阅读。如果这个反转逻辑在多处使用,代码会重复。

使用计算属性:

<div id="app">
  <p>原始字符串: {{ message }}</p>
  <p>反转字符串: {{ reversedMessage }}</p>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  computed: {
    reversedMessage: function() {
      return this.message.split('').reverse().join('')
    }
  }
})
</script>

代码更清晰,逻辑更易维护。

计算属性 vs 方法

你可能注意到,用方法也能达到同样效果:

<p>反转字符串: {{ reverseMessage() }}</p>

<script>
new Vue({
  methods: {
    reverseMessage: function() {
      return this.message.split('').reverse().join('')
    }
  }
})
</script>

两者的区别在于缓存

计算属性基于它们的响应式依赖进行缓存

  • 计算属性只有在相关依赖发生改变时才会重新求值
  • 多次访问 reversedMessage 会立即返回缓存结果,不必再次执行函数
  • 方法调用总是会执行函数
computed: {
  now: function() {
    return Date.now()
  }
}

上面的计算属性不会更新,因为 Date.now() 不是响应式依赖。

计算属性的 setter

计算属性默认只有 getter,需要时也可以提供 setter:

<div id="app">
  <p>全名: {{ fullName }}</p>
  <button @click="fullName = 'John Doe'">设置为 John Doe</button>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  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]
      }
    }
  }
})
</script>

当运行 this.fullName = 'John Doe' 时,setter 会被调用,firstNamelastName 会被相应更新。

实际应用场景

1. 格式化显示

computed: {
  formattedPrice: function() {
    return '¥' + this.price.toFixed(2)
  },
  
  formattedDate: function() {
    var date = new Date(this.timestamp)
    return date.getFullYear() + '-' + 
           (date.getMonth() + 1) + '-' + 
           date.getDate()
  }
}

2. 过滤和排序

computed: {
  activeUsers: function() {
    return this.users.filter(function(user) {
      return user.isActive
    })
  },
  
  sortedProducts: function() {
    return this.products.slice().sort(function(a, b) {
      return a.price - b.price
    })
  }
}

3. 组合多个数据

computed: {
  cartTotal: function() {
    return this.cartItems.reduce(function(total, item) {
      return total + item.price * item.quantity
    }, 0)
  },
  
  cartItemCount: function() {
    return this.cartItems.reduce(function(count, item) {
      return count + item.quantity
    }, 0)
  }
}

4. 验证表单

computed: {
  isFormValid: function() {
    return this.username.length >= 3 &&
           this.email.includes('@') &&
           this.password.length >= 6
  },
  
  usernameError: function() {
    if (this.username.length === 0) return '请输入用户名'
    if (this.username.length < 3) return '用户名至少3个字符'
    return ''
  }
}

5. 条件样式

computed: {
  buttonClass: function() {
    return {
      'btn-primary': this.isValid,
      'btn-disabled': !this.isValid,
      'btn-loading': this.isLoading
    }
  },
  
  progressStyle: function() {
    return {
      width: this.progress + '%',
      backgroundColor: this.progress > 80 ? '#4caf50' : '#2196f3'
    }
  }
}

计算属性传参

计算属性本身不能传参,但可以通过返回函数实现:

<div v-for="item in items" :key="item.id">
  <span>{{ getItemTotal(item.id) }}</span>
</div>

<script>
new Vue({
  computed: {
    // 这不是真正的计算属性传参,而是返回一个函数
    getItemTotal: function() {
      var self = this
      return function(id) {
        var item = self.items.find(function(i) {
          return i.id === id
        })
        return item ? item.price * item.quantity : 0
      }
    }
  }
})
</script>

注意:这种方式返回的函数不会被缓存。如果需要缓存,考虑使用方法代替。

计算属性 vs 侦听器

当需要在数据变化时执行异步或开销较大的操作时,使用侦听器更合适:

// 使用计算属性(适合简单转换)
computed: {
  fullName: function() {
    return this.firstName + ' ' + this.lastName
  }
}

// 使用侦听器(适合异步操作)
watch: {
  searchQuery: function(newVal) {
    this.isLoading = true
    this.debouncedSearch(newVal)
  }
}

性能优化

计算属性的缓存特性可以用于性能优化:

computed: {
  // 这个计算属性只会在 items 改变时重新计算
  expensiveResult: function() {
    // 复杂的计算
    return this.items.map(function(item) {
      // 一些耗时的操作
    }).filter(function(item) {
      // 过滤
    }).reduce(function(acc, item) {
      // 聚合
    }, 0)
  }
}

如果用方法,每次调用都会重新计算。

小结

特性计算属性方法
缓存
调用方式{{ prop }}{{ method() }}
适用场景数据转换、格式化需要每次执行、传参

计算属性是 Vue 响应式系统的重要组成部分,合理使用可以让代码更简洁高效。