Computed Property-计算属性对象

定义

计算属性是一个“基于其他数据动态计算得出的值”,它具有响应性(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
})
  1. JavaScript 引擎执行 computed(...) 函数调用
    • 把你的箭头函数作为参数传进去。
  2. computed() 内部:
    • 创建一个空的 ComputedRef 对象(比如 { value: undefined, ... }
    • 把你的箭头函数保存起来(比如存到 _getter 属性)
    • 但不立即执行它!
  3. 返回这个 ComputedRef 对象,赋值给 fullName

✅ 所以:

  • fullName 确实已经是一个对象了(类型是 ComputedRef<string>
  • 但它的 .value 还是 undefined(未计算)
  • 你的箭头函数只是被“记住”了,还没运行

第 3 步:首次访问 fullName.value —— 触发计算

console.log(fullName.value) // "张三"
  1. 访问 fullName.value → 触发 ComputedRef 对象的 .value getter
  2. getter 内部检查:
    • “我有没有缓存值?” → 没有(第一次)
    • “那我要执行保存的箭头函数!”
  3. 执行你的箭头函数
    • 读取 user.firstName → Vue 记录依赖:“这个计算属性依赖了 user.firstName
    • 读取 user.lastName → 同样记录依赖
    • 返回 "张三"
  4. 把结果缓存起来(比如 ._value = "张三"
  5. 返回缓存值

✅ 所以:第一次 .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 中读取了 响应式数据(如 reactiveref、其他 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 是普通常量,没问题,因为我们不期望它变

×

喜欢就点赞,疼爱就打赏