Vue.js 计算属性 (computed) 学习笔记
计算属性是 Vue 中处理派生数据的核心机制。当数据依赖于其他响应式数据时,使用计算属性可以自动追踪依赖、缓存结果,并保持模板的简洁性。
一、核心概念
1. 什么是计算属性?
计算属性是基于它们的响应式依赖进行缓存的。只有当依赖的响应式数据发生变化时,计算属性才会重新求值。
<template> <p>原始消息:{{ message }}</p> <p>反转消息:{{ reversedMessage }}</p> </template> <script setup> import { ref, computed } from 'vue' const message = ref('Hello Vue') // 计算属性:自动缓存,依赖 message 变化时才重新计算 const reversedMessage = computed(() => { return message.value.split('').reverse().join('') }) </script>2. 为什么使用计算属性?
- 缓存性:只有依赖变化时才重新计算,性能优于方法。
- 声明式:模板中直接调用,逻辑清晰。
- 可维护性:复杂逻辑抽离到
computed,模板保持简洁。
二、只读计算属性
最基本的用法,仅用于读取派生数据。
<template> <div> <p>价格:{{ price }}</p> <p>折扣价 (8 折):{{ discountPrice }}</p> <p>含税价:{{ taxPrice }}</p> </div> </template> <script setup> import { ref, computed } from 'vue' const price = ref(100) const taxRate = 0.1 // 计算属性 const discountPrice = computed(() => price.value * 0.8) const taxPrice = computed(() => price.value * (1 + taxRate)) </script>注意:在模板中调用计算属性不需要加括号,因为它是一个属性而非方法。
三、可写计算属性 (Getter + Setter)
当需要双向绑定派生数据时,可以定义get和set。
场景:拆分全名为姓和名
<template> <div> <p>全名:{{ fullName }}</p> <input v-model="firstName" placeholder="姓" /> <input v-model="lastName" placeholder="名" /> </div> </template> <script setup> import { ref, computed } from 'vue' const firstName = ref('张') const lastName = ref('三') // 可写计算属性 const fullName = computed({ // 读取时:拼接姓和名 get() { return firstName.value + lastName.value }, // 写入时:拆分全名 set(newValue) { // 假设全名格式为 "姓 名" const [first, last] = newValue.split(' ') firstName.value = first || '' lastName.value = last || '' } }) </script>原理:
v-model="fullName"会触发set方法。- 当
firstName或lastName变化时,get方法自动重新计算。
四、computedvsmethods
| 特性 | computed | methods |
|---|---|---|
| 缓存 | ✅ 有缓存(依赖不变不重算) | ❌ 无缓存(每次调用都执行) |
| 调用方式 | {{ fullName }}(无括号) | {{ getFullName() }}(需括号) |
| 依赖追踪 | 自动追踪响应式依赖 | 无自动追踪 |
| 适用场景 | 需要缓存的派生数据 | 每次都需要重新计算、或涉及副作用 |
对比示例
<template> <div> <!-- 计算属性:缓存,依赖 message 变化才重算 --> <p>计算属性:{{ reversedMessage }}</p> <!-- 方法:每次渲染都执行 --> <p>方法:{{ getReversedMessage() }}</p> </div> </template> <script setup> import { ref, computed } from 'vue' const message = ref('Hello') // 计算属性 const reversedMessage = computed(() => { console.log('计算属性执行') return message.value.split('').reverse().join('') }) // 方法 function getReversedMessage() { console.log('方法执行') return message.value.split('').reverse().join('') } </script>输出观察:
- 初始渲染:计算属性执行 1 次,方法执行 1 次。
- 再次渲染(无数据变化):计算属性不执行,方法执行。
message变化:计算属性执行 1 次,方法执行 1 次。
结论:对于复杂的计算,优先使用
computed以提升性能。
五、计算属性的依赖追踪
计算属性会自动追踪其内部使用的所有响应式数据(ref、reactive、props等)。
<script setup> import { ref, computed } from 'vue' const firstName = ref('张') const lastName = ref('三') const age = ref(25) // 依赖 firstName 和 lastName const fullName = computed(() => firstName.value + lastName.value) // 依赖 age const isAdult = computed(() => age.value >= 18) // 依赖多个计算属性 const description = computed(() => { return `${fullName.value} 今年 ${age.value} 岁,${isAdult.value ? '成年' : '未成年'}` }) </script>注意:非响应式数据(如普通变量)不会被追踪。
六、实战示例
1. 购物车总价计算
<template> <div> <ul> <li v-for="item in cartItems" :key="item.id"> {{ item.name }} x {{ item.quantity }} = ¥{{ (item.price * item.quantity).toFixed(2) }} </li> </ul> <p>商品数量:{{ totalQuantity }}</p> <p>总价:¥{{ totalPrice }}</p> </div> </template> <script setup> import { ref, computed } from 'vue' const cartItems = ref([ { id: 1, name: 'iPhone', price: 6999, quantity: 1 }, { id: 2, name: 'AirPods', price: 1899, quantity: 2 } ]) // 计算总数量 const totalQuantity = computed(() => { return cartItems.value.reduce((sum, item) => sum + item.quantity, 0) }) // 计算总价 const totalPrice = computed(() => { return cartItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0) }) </script>2. 列表过滤与排序
<template> <div> <input v-model="searchQuery" placeholder="搜索..." /> <ul> <li v-for="user in filteredUsers" :key="user.id"> {{ user.name }} ({{ user.age }}岁) </li> </ul> </div> </template> <script setup> import { ref, computed } from 'vue' const users = ref([ { id: 1, name: '张三', age: 25 }, { id: 2, name: '李四', age: 30 }, { id: 3, name: '王五', age: 28 } ]) const searchQuery = ref('') // 过滤 + 排序 const filteredUsers = computed(() => { let result = users.value // 过滤 if (searchQuery.value) { result = result.filter(user => user.name.includes(searchQuery.value) ) } // 排序(按年龄) result = result.sort((a, b) => a.age - b.age) return result }) </script>3. 表单验证
<template> <div> <input v-model="username" placeholder="用户名" /> <p v-if="usernameError" style="color: red">{{ usernameError }}</p> <input v-model="password" type="password" placeholder="密码" /> <p v-if="passwordError" style="color: red">{{ passwordError }}</p> <button :disabled="!isFormValid">提交</button> </div> </template> <script setup> import { ref, computed } from 'vue' const username = ref('') const password = ref('') // 用户名验证 const usernameError = computed(() => { if (!username.value) return '用户名不能为空' if (username.value.length < 3) return '用户名至少 3 个字符' return '' }) // 密码验证 const passwordError = computed(() => { if (!password.value) return '密码不能为空' if (password.value.length < 6) return '密码至少 6 个字符' return '' }) // 表单是否有效 const isFormValid = computed(() => { return !usernameError.value && !passwordError.value }) </script>七、最佳实践
- 保持简洁:计算属性逻辑应简单清晰,避免复杂副作用。
- 逻辑外移:复杂逻辑可抽离为独立的函数,在
computed中调用。 - 避免副作用:不要在
computed中修改其他响应式数据(如ref.value = ...),这会导致无限循环。 - 优先使用
computed:对于派生数据,优先使用computed而非methods。 - 命名规范:计算属性通常以名词命名,如
fullName、totalPrice、filteredUsers。
八、常见陷阱
陷阱 1:在computed中修改响应式数据
<!-- ❌ 错误:导致无限循环 --> const doubleCount = computed(() => { count.value *= 2 // 修改依赖,触发重新计算,无限循环 return count.value * 2 })陷阱 2:使用非响应式数据
<script setup> import { ref, computed } from 'vue' const count = ref(0) const multiplier = 2 // 普通变量,非响应式 // ❌ 修改 multiplier 不会触发重新计算 const doubled = computed(() => count.value * multiplier) </script>修正:将multiplier改为ref。
const multiplier = ref(2) const doubled = computed(() => count.value * multiplier.value)陷阱 3:忘记.value
在<script setup>中访问ref时,必须使用.value。
<script setup> import { ref, computed } from 'vue' const name = ref('Vue') // ❌ 错误:name 是 ref 对象,不是字符串 const upper = computed(() => name.toUpperCase()) // ✅ 正确 const upper = computed(() => name.value.toUpperCase()) </script>计算属性是 Vue 响应式系统的核心特性之一,合理使用可以大幅提升代码的可读性和性能。