定义
计算属性是一个“基于其他数据动态计算得出的值”,它具有响应性(reactive)和缓存性(cached)。在 Vue 3 中,计算属性(computed)返回的是一个对象,更准确地说,是一个 响应式引用对象(Reactive Reference Object),具体类型是 ComputedRef<T>。
- 它看起来像一个普通属性(比如
fullName),但它的值不是直接存储的,而是通过函数计算出来的。 - 当它依赖的数据发生变化时,它的值会自动重新计算。
- 只要依赖不变,多次读取不会重复执行计算逻辑(缓存优化)。
三种对象类型
| 类型 | 创建方式 | 是否响应式 | 是否可写 | 是否缓存 | 用途 |
|---|---|---|---|---|---|
| 1. 普通对象 | { count: 1 } |
❌ 否 | ✅ 是 | ❌ 否 | 临时数据,不用于 UI |
| 2. 响应式对象 | reactive({ count: 1 }) |
✅ 是 | ✅ 是 | ❌ 否 | 存储可变状态 |
| 3. 计算属性对象 | computed(() => ...) |
✅ 是 | ❌ 只读 | ✅ 是 | 派生状态(基于其他响应式数据计算得出) |
普通对象(❌ 不推荐用于 UI)
// 普通对象
const user = {
firstName: '张',
lastName: '三'
}
// 手动计算 fullName
const fullName = user.firstName + user.lastName
console.log(fullName) // "张三"
// 修改数据
user.firstName = '李'
console.log(fullName) // 仍然是 "张三"!❌ 不会自动更新
问题:fullName 是一个静态字符串,和 user 脱节了。数据变了,它不知道。
使用 响应式对象(✅ 可写,但需手动维护派生值)
import { reactive } from 'vue'
// 响应式对象
const user = reactive({
firstName: '张',
lastName: '三',
// 手动添加 fullName
get fullName() {
console.log('计算 fullName...')
return this.firstName + this.lastName
}
})
console.log(user.fullName) // "张三"(打印日志)
console.log(user.fullName) // "张三"(**再次打印日志!** ❌ 无缓存)
user.firstName = '李'
console.log(user.fullName) // "李三"(打印日志)
问题:
- 每次访问
fullName都会重新计算(无缓存) - 如果逻辑复杂(如过滤 1000 条数据),性能差
- 虽然能响应变化,但效率低
使用 计算属性对象(✅ 推荐!)
import { reactive, computed } from 'vue'
// 原始响应式数据
const user = reactive({
firstName: '张',
lastName: '三'
})
// 计算属性:基于 user 的派生值
const fullName = computed(() => {
console.log('计算 fullName...')
return user.firstName + user.lastName
})
// 第一次访问 → 触发计算
console.log(fullName.value) // "张三"(打印日志)
// 第二次访问 → 使用缓存
console.log(fullName.value) // "张三"(**不再打印日志!** ✅ 有缓存)
// 修改依赖数据
user.firstName = '李'
// 依赖变了 → 重新计算
console.log(fullName.value) // "李三"(打印日志)
关键区别总结(重点!)
| 对比项 | 普通对象 | 响应式对象(带 getter) | 计算属性对象 |
|---|---|---|---|
| 是否自动响应数据变化? | ❌ 否 | ✅ 是 | ✅ 是 |
| 是否缓存计算结果? | ❌(无计算) | ❌ 否(每次调用 getter 都执行) | ✅ 是(依赖不变就不重算) |
| 是否只读? | — | ✅(getter 只读) | ✅ 是(默认只读) |
| 是否自动追踪依赖? | ❌ | ❌(需手动写逻辑) | ✅ 是(Vue 自动分析 user.firstName 等依赖) |
| 适合复杂计算吗? | ❌ | ⚠️ 性能差 | ✅ 非常适合 |
computed到底做了什么
import { reactive, computed } from 'vue'
// 原始响应式数据
const user = reactive({
firstName: '张',
lastName: '三'
})
// 计算属性:基于 user 的派生值
const fullName = computed(() => {
console.log('计算 fullName...')
return user.firstName + user.lastName
})
// 第一次访问 → 触发计算
console.log(fullName.value) // "张三"(打印日志)
// 第二次访问 → 使用缓存
console.log(fullName.value) // "张三"(**不再打印日志!** ✅ 有缓存)
// 修改依赖数据
user.firstName = '李'
// 依赖变了 → 重新计算
console.log(fullName.value) // "李三"(打印日志)
- ✅
const fullName = computed(...)这行代码执行时,computed()函数被调用,返回一个响应式对象(ComputedRef)。 - ❌ 但你传入的箭头函数
() => {...}此时并没有执行! - ✅ 箭头函数只在首次访问
fullName.value(或依赖变化后再次访问)时才执行。 - ✅ 这种行为叫做 “懒求值”(Lazy Evaluation)。
代码执行流程
第 1 步:定义响应式数据
const user = reactive({ firstName: '张', lastName: '三' })
- 创建了一个响应式对象
user。 - Vue 内部用
Proxy包装它,准备追踪读写操作。
✅ 此时:没有计算 fullName,甚至还没定义它。
第 2 步:调用 computed() —— 创建计算属性对象
const fullName = computed(() => {
console.log('计算 fullName...')
return user.firstName + user.lastName
})
- JavaScript 引擎执行
computed(...)函数调用- 把你的箭头函数作为参数传进去。
computed()内部:- 创建一个空的
ComputedRef对象(比如{ value: undefined, ... }) - 把你的箭头函数保存起来(比如存到
_getter属性) - 但不立即执行它!
- 创建一个空的
- 返回这个
ComputedRef对象,赋值给fullName
✅ 所以:
fullName确实已经是一个对象了(类型是ComputedRef<string>)- 但它的
.value还是undefined(未计算) - 你的箭头函数只是被“记住”了,还没运行
第 3 步:首次访问 fullName.value —— 触发计算
console.log(fullName.value) // "张三"
- 访问
fullName.value→ 触发ComputedRef对象的.valuegetter - getter 内部检查:
- “我有没有缓存值?” → 没有(第一次)
- “那我要执行保存的箭头函数!”
- 执行你的箭头函数:
- 读取
user.firstName→ Vue 记录依赖:“这个计算属性依赖了user.firstName” - 读取
user.lastName→ 同样记录依赖 - 返回
"张三"
- 读取
- 把结果缓存起来(比如
._value = "张三") - 返回缓存值
✅ 所以:第一次 .value 才真正执行箭头函数,并建立依赖关系!
第 4 步:再次访问fullName.value —— 使用缓存
console.log(fullName.value) // "张三"(无日志)
- getter 发现:依赖没变 + 有缓存 → 直接返回缓存值
- 不执行箭头函数 → 所以看不到
console.log
第 5 步:修改依赖 → 标记为“脏”
user.firstName = '李'
- 修改
user.firstName→ 触发reactive的 setter - Vue 检查:“谁依赖了
firstName?” → 发现fullName依赖它 - 把
fullName标记为“需要重新计算”(dirty)
第 6 步:再次访问 → 重新计算
console.log(fullName.value) // "李三"
- getter 发现:依赖变了(dirty) → 重新执行箭头函数
- 再次记录依赖(虽然没变),更新缓存,返回新值
用伪代码模拟 computed 内部实现
function computed(getter) {
let _value;
let _dirty = true; // 是否需要重新计算
let _deps = new Set(); // 依赖集合(简化)
const runner = () => {
// 执行 getter,同时收集依赖(简化)
_value = getter();
_dirty = false;
};
return {
get value() {
if (_dirty) {
runner(); // 只有需要时才执行
}
return _value;
},
// ...其他内部属性
};
}
✅ 关键:.value 的 getter 里才决定是否执行 runner()(即你的箭头函数)
computed 的响应性依赖于 Vue 的响应式系统:
✅ 如果你在 computed 中读取了 响应式数据(如 reactive、ref、其他 computed),→ 当这些数据变化时,computed 会自动重新计算。
import { reactive, computed } from 'vue'
const user = reactive({ name: '张三' })
const greeting = computed(() => {
return 'Hello, ' + user.name // ✅ 依赖响应式对象
})
console.log(greeting.value) // "Hello, 张三"
user.name = '李四'
console.log(greeting.value) // "Hello, 李四" → ✅ 自动更新!
❌ 如果你只读取了 普通变量/常量,→ 它们的变化 不会被追踪,computed 不会重新计算。
import { computed } from 'vue'
let name = '张三' // ❌ 普通变量,非响应式
const greeting = computed(() => {
return 'Hello, ' + name // 读取普通变量
})
console.log(greeting.value) // "Hello, 张三"
name = '李四' // 修改普通变量
console.log(greeting.value) // 仍然是 "Hello, 张三" ❌ 不更新!
原因:Vue 无法追踪普通变量的变化,因为它们不在响应式系统中。
也可以在 computed 里用普通值
const API_URL = 'https://api.example.com' // 全局常量
const user = reactive({ id: 123 })
// 在 computed 中拼接 URL
const userApiUrl = computed(() => {
return `${API_URL}/users/${user.id}` // ✅ 常量 + 响应式
})
// 当 user.id 变化时,URL 自动更新
user.id = 456
console.log(userApiUrl.value) // "https://api.example.com/users/456"
这里 API_URL 是普通常量,没问题,因为我们不期望它变。