Vue 2 与 Vue 3 计算属性完全指南:从 Options API 到组合式 API 的演进

前言

在前几篇博客中,我们详细聊了 Vue 的响应式 API(ref/reactive)和侦听器(watch)。今天,我们把目光投向 Vue 中另一个核心概念------计算属性(computed)

如果说 watch 是"当数据变化时做点什么",那么 computed 就是"根据已有数据计算出新数据 "。它们是 Vue 响应式系统中分工明确的两个工具:一个负责派生数据 ,一个负责执行副作用

本文将系统梳理 Vue 2 和 Vue 3 中 computed 的用法差异、底层原理、实战案例和最佳实践,并通过与 methodswatch 的深度对比,帮你彻底掌握什么时候该用谁。

一、什么是 computed?为什么需要它?

1.1 从模板中的表达式说起

在 Vue 模板中,我们可以直接使用 JavaScript 表达式:

vue 复制代码
<template>
  <p>{{ message.split('').reverse().join('') }}</p>
</template>

这种写法虽然方便,但当逻辑变得复杂时,模板会变得臃肿且难以维护 。更重要的是,如果多个地方都需要同样的计算结果,代码就会重复

1.2 computed 的核心价值

计算属性(computed)就是为解决这两个问题而生的:

  1. 逻辑复用:将复杂的计算逻辑封装成一个属性,在模板中多次使用
  2. 缓存机制基于依赖进行缓存,只有依赖变化时才重新计算
  3. 声明式编程:描述"数据应该如何计算",而非"什么时候计算"

一句话总结computed 用于根据已有的响应式数据派生出新的数据 ,并且这个派生结果会被缓存,直到依赖发生变化。

二、Vue 2 中的 computed(Options API)

2.1 基本用法:只读计算属性

在 Vue 2 中,computed 是一个选项对象 ,与 datamethods 平级。计算属性的值是一个函数,函数的返回值就是计算属性的值。

javascript 复制代码
export default {
  data() {
    return {
      firstName: '张',
      lastName: '三',
      price: 100,
      quantity: 3
    }
  },
  computed: {
    // 基础用法:拼接全名
    fullName() {
      return this.firstName + ' ' + this.lastName
    },
    // 复杂计算:商品总价
    totalPrice() {
      return this.price * this.quantity
    },
    // 带条件判断的计算
    discountPrice() {
      const total = this.price * this.quantity
      return total > 200 ? total * 0.9 : total
    }
  }
}

在模板中使用时,不需要加括号,就像使用普通属性一样:

vue 复制代码
<template>
  <p>全名:{{ fullName }}</p>
  <p>总价:{{ totalPrice }}</p>
  <p>折后价:{{ discountPrice }}</p>
</template>

2.2 可写计算属性:提供 setter

计算属性默认是只读的------你只能获取它的值,不能修改它。但在某些场景下(如表单的双向绑定),你可能需要一个"可写"的计算属性。

javascript 复制代码
export default {
  data() {
    return {
      firstName: '张',
      lastName: '三'
    }
  },
  computed: {
    fullName: {
      // getter:读取时触发
      get() {
        return this.firstName + ' ' + this.lastName
      },
      // setter:赋值时触发
      set(newVal) {
        const names = newVal.split(' ')
        this.firstName = names[0]
        this.lastName = names[names.length - 1]
      }
    }
  }
}
vue 复制代码
<template>
  <!-- 修改 fullName 会自动拆分到 firstName 和 lastName -->
  <input v-model="fullName" placeholder="输入全名" />
  <p>姓:{{ firstName }},名:{{ lastName }}</p>
</template>

2.3 computed vs data:不要重复定义

计算属性不能与 data 中的属性重名 ,否则会造成冲突。计算属性的值由函数计算得出,不需要在 data 中预先定义。

2.4 底层原理:基于 Watcher 的缓存机制

Vue 2 的计算属性底层依赖于 Watcher 机制:

  1. 初始化 :Vue 在 initState 阶段为每个计算属性创建一个计算属性 Watcher
  2. 依赖收集 :首次访问计算属性时,将当前 Watcher 设置为 Dep.target,执行 getter 函数时收集依赖
  3. 缓存机制 :每个计算属性 Watcher 都有一个 dirty 标志
    • dirty = true:需要重新计算
    • dirty = false:直接返回缓存值
  4. 触发更新 :当依赖的响应式数据变化时,通知计算属性 Watcher,将 dirty 设为 true
  5. 重新计算 :下次访问计算属性时,发现 dirty = true,重新执行 getter 并更新缓存

这种设计保证了:只要依赖没有变化,多次访问计算属性都会立即返回缓存值,不会重复执行计算逻辑

三、Vue 3 中的 computed(组合式 API)

3.1 基本用法

在 Vue 3 中,computed 是一个可导入的函数 ,在 setup()<script setup> 中使用。

使用 computed() 函数

vue 复制代码
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')
const price = ref(100)
const quantity = ref(3)

// 只读计算属性:传入一个 getter 函数
const fullName = computed(() => {
  return firstName.value + ' ' + lastName.value
})

const totalPrice = computed(() => {
  return price.value * quantity.value
})

// 复杂的计算逻辑
const discountPrice = computed(() => {
  const total = price.value * quantity.value
  return total > 200 ? total * 0.9 : total
})
</script>
vue 复制代码
<template>
  <!-- 模板中自动解包,不需要 .value -->
  <p>全名:{{ fullName }}</p>
  <p>总价:{{ totalPrice }}</p>
</template>

注意 :在 <script> 中访问 computed 的值需要 .value(因为 computed() 返回的是一个 ref 对象),但在模板中会自动解包。

3.2 可写计算属性:传入对象

与 Vue 2 类似,Vue 3 也支持可写的计算属性:

vue 复制代码
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 可写计算属性:传入包含 get 和 set 的对象
const fullName = computed({
  // getter
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter
  set(newVal) {
    const names = newVal.split(' ')
    firstName.value = names[0]
    lastName.value = names[names.length - 1]
  }
})
</script>

3.3 Vue 2 与 Vue 3 的语法对比

对比维度 Vue 2(Options API) Vue 3(组合式 API)
定义方式 computed 选项对象 computed() 函数
导入 无需导入,直接使用 需要 import { computed } from 'vue'
返回值 直接作为 this 的属性访问 返回一个 ref 对象 ,需 .value 访问
模板访问 直接使用 {``{ fullName }} 自动解包,直接使用 {``{ fullName }}
可写计算属性 { get() {}, set() {} } { get() {}, set() {} }(语法相同)

3.4 底层原理:基于 Proxy 和 Effect 的懒更新

Vue 3 的计算属性基于 ProxyEffect 机制实现:

核心数据结构

javascript 复制代码
// 简化版 ComputedRefImpl
class ComputedRefImpl {
  constructor(getter) {
    this._dirty = true      // 脏标记:是否需要重新计算
    this._value = undefined // 缓存的值
    // 创建 effect,scheduler 在依赖变化时触发
    this.effect = new ReactiveEffect(getter, () => {
      this._dirty = true    // 依赖变化时标记为"脏"
    })
  }
  
  get value() {
    if (this._dirty) {
      this._value = this.effect.run()  // 仅在脏时重新计算
      this._dirty = false
    }
    return this._value
  }
}

关键机制------懒更新(Lazy Evaluation)

Vue 3 的 computed 采用懒更新 策略------只在计算属性被访问时才重新计算,而不是依赖一变化就立即计算。

这种设计的优势在于:

  • 如果计算属性在某个渲染周期中没有被使用,即使依赖变化了也不会浪费性能去计算
  • 多个依赖变化时,只会在最后一次被访问时计算一次,避免了中间状态的多次计算

脏标记(_dirty)机制

  • 初始状态:_dirty = true,首次访问时计算
  • 依赖变化:触发 scheduler,设置 _dirty = true
  • 再次访问:检查 _dirty,为 true 时重新计算并缓存

四、真实项目实战

4.1 场景一:购物车计算

这是 computed 最经典的实战场景------计算购物车的商品数量、总价、优惠等。

Vue 3 实现

vue 复制代码
<template>
  <div class="shopping-cart">
    <h2>购物车</h2>
    
    <!-- 商品列表 -->
    <div v-for="item in cartItems" :key="item.id" class="cart-item">
      <span>{{ item.name }}</span>
      <button @click="decreaseQuantity(item)">-</button>
      <span>{{ item.quantity }}</span>
      <button @click="increaseQuantity(item)">+</button>
      <span>¥{{ item.price * item.quantity }}</span>
      <button @click="removeItem(item.id)">删除</button>
    </div>
    
    <!-- 汇总信息(全部由 computed 计算) -->
    <div class="summary">
      <p>商品总数:{{ totalQuantity }} 件</p>
      <p>商品总价:¥{{ totalPrice }}</p>
      <p v-if="discount > 0">优惠:-¥{{ discount }}</p>
      <p>实付金额:<strong>¥{{ finalPrice }}</strong></p>
      <p v-if="isFreeShipping">🎉 已满足免运费条件</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

// ====== 原始数据 ======
const cartItems = ref([
  { id: 1, name: 'Vue 实战指南', price: 89, quantity: 1 },
  { id: 2, name: '机械键盘', price: 299, quantity: 1 },
  { id: 3, name: '蓝牙耳机', price: 199, quantity: 0 }
])

// ====== 操作方法 ======
const increaseQuantity = (item) => {
  item.quantity++
}

const decreaseQuantity = (item) => {
  if (item.quantity > 1) {
    item.quantity--
  }
}

const removeItem = (id) => {
  cartItems.value = cartItems.value.filter(item => item.id !== id)
}

// ====== 计算属性 ======
// 1. 筛选有效商品(数量 > 0)
const validItems = computed(() => {
  return cartItems.value.filter(item => item.quantity > 0)
})

// 2. 商品总数
const totalQuantity = computed(() => {
  return validItems.value.reduce((sum, item) => sum + item.quantity, 0)
})

// 3. 商品总价
const totalPrice = computed(() => {
  return validItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})

// 4. 优惠金额(满300减30)
const discount = computed(() => {
  if (totalPrice.value >= 300) return 30
  if (totalPrice.value >= 200) return 15
  return 0
})

// 5. 最终价格
const finalPrice = computed(() => {
  return totalPrice.value - discount.value
})

// 6. 是否免运费(满200免运费)
const isFreeShipping = computed(() => {
  return totalPrice.value >= 200
})
</script>

为什么用 computed 而不是 methods? 购物车的汇总信息会在用户增删商品时频繁变化,使用 computed 可以确保只有依赖变化时才重新计算 ,而且多个汇总属性之间可以相互依赖 (如 discount 依赖 totalPricefinalPrice 依赖 totalPricediscount),这是 methods 难以优雅实现的。

4.2 场景二:表单联动计算

在实际项目中,表单字段之间经常存在联动关系------选择一个值,自动计算另一个值。

vue 复制代码
<template>
  <div class="order-form">
    <h2>订单计算</h2>
    
    <div>
      <label>商品单价:</label>
      <input type="number" v-model="unitPrice" />
    </div>
    
    <div>
      <label>购买数量:</label>
      <input type="number" v-model="quantity" />
    </div>
    
    <div>
      <label>折扣率(%):</label>
      <input type="number" v-model="discountRate" min="0" max="100" />
    </div>
    
    <!-- 自动计算的结果 -->
    <div class="result">
      <p>小计:¥{{ subtotal }}</p>
      <p>折扣金额:-¥{{ discountAmount }}</p>
      <p>应付金额:<strong>¥{{ finalAmount }}</strong></p>
      <p v-if="isHighValue">💰 大额订单,已为您升级 VIP 服务</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const unitPrice = ref(100)
const quantity = ref(2)
const discountRate = ref(10)  // 10%

// 小计
const subtotal = computed(() => {
  return unitPrice.value * quantity.value
})

// 折扣金额
const discountAmount = computed(() => {
  return (subtotal.value * discountRate.value) / 100
})

// 最终金额
const finalAmount = computed(() => {
  return subtotal.value - discountAmount.value
})

// 是否为大额订单(> 500)
const isHighValue = computed(() => {
  return finalAmount.value > 500
})
</script>

4.3 场景三:待办事项的统计

待办事项列表是计算属性的经典应用场景------根据原始列表派生出各种统计数据。

vue 复制代码
<template>
  <div class="todo-app">
    <h2>我的待办</h2>
    
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加新待办" />
    
    <ul>
      <li v-for="todo in filteredTodos" :key="todo.id">
        <input type="checkbox" v-model="todo.done" />
        <span :class="{ done: todo.done }">{{ todo.text }}</span>
        <button @click="deleteTodo(todo.id)">删除</button>
      </li>
    </ul>
    
    <!-- 统计信息:全部由 computed 计算 -->
    <div class="stats">
      <span>全部:{{ total }}</span>
      <span>已完成:{{ completed }}</span>
      <span>未完成:{{ pending }}</span>
      <span>完成率:{{ completionRate }}%</span>
    </div>
    
    <!-- 筛选按钮 -->
    <div class="filters">
      <button @click="filter = 'all'" :class="{ active: filter === 'all' }">全部</button>
      <button @click="filter = 'active'" :class="{ active: filter === 'active' }">未完成</button>
      <button @click="filter = 'completed'" :class="{ active: filter === 'completed' }">已完成</button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

// ====== 数据 ======
const todos = ref([
  { id: 1, text: '学习 Vue 3 计算属性', done: true },
  { id: 2, text: '写一篇博客', done: false },
  { id: 3, text: '分享给朋友', done: false }
])

const newTodo = ref('')
const filter = ref('all')
let nextId = 4

// ====== 方法 ======
const addTodo = () => {
  if (newTodo.value.trim()) {
    todos.value.push({
      id: nextId++,
      text: newTodo.value.trim(),
      done: false
    })
    newTodo.value = ''
  }
}

const deleteTodo = (id) => {
  todos.value = todos.value.filter(t => t.id !== id)
}

// ====== 计算属性(核心) ======
// 1. 基础统计
const total = computed(() => todos.value.length)
const completed = computed(() => todos.value.filter(t => t.done).length)
const pending = computed(() => todos.value.filter(t => !t.done).length)
const completionRate = computed(() => {
  if (total.value === 0) return 0
  return Math.round((completed.value / total.value) * 100)
})

// 2. 筛选后的列表
const filteredTodos = computed(() => {
  switch (filter.value) {
    case 'active':
      return todos.value.filter(t => !t.done)
    case 'completed':
      return todos.value.filter(t => t.done)
    default:
      return todos.value
  }
})

// 3. 是否全部完成
const allDone = computed(() => {
  return total.value > 0 && completed.value === total.value
})
</script>

五、computed vs methods vs watch:全面对比与选择指南

5.1 核心区别速览

对比维度 computed methods watch
核心定位 派生数据 执行操作 响应变化
缓存机制 ✅ 有缓存 ❌ 无缓存 ❌ 无缓存
调用方式 自动(依赖变化时) 手动调用 自动(监听数据变化时)
返回值 必须 return 可选 return 不需要 return
适用场景 数据计算、派生 事件处理、逻辑复用 异步操作、副作用
模板中使用 作为属性 {``{ fullName }} 作为方法 {``{ format() }} 不在模板中使用

5.2 computed vs methods

相同点:两者都可以在模板中调用并返回计算结果。

核心区别缓存

vue 复制代码
<template>
  <div>
    <!-- computed:依赖没变,直接从缓存读取,不会重复执行 -->
    <p>{{ computedValue }}</p>
    <p>{{ computedValue }}</p>
    <p>{{ computedValue }}</p>
    
    <!-- methods:每次调用都会重新执行函数 -->
    <p>{{ methodValue() }}</p>
    <p>{{ methodValue() }}</p>
    <p>{{ methodValue() }}</p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const count = ref(1)

// computed:有缓存,只有 count 变化时才重新计算
const computedValue = computed(() => {
  console.log('computed 执行了')
  return count.value * 2
})

// methods:每次调用都执行
const methodValue = () => {
  console.log('method 执行了')
  return count.value * 2
}
</script>

选择建议

场景 推荐 原因
需要派生数据,且会被多次访问 computed 利用缓存,避免重复计算
需要传参进行计算 methods computed 不支持传参
计算开销大,但依赖变化不频繁 computed 缓存能显著提升性能
需要主动触发的逻辑(如点击事件) methods 这是方法的天然职责

5.3 computed vs watch

相同点:两者都基于 Vue 的依赖追踪机制,都能响应数据变化。

核心区别

对比维度 computed watch
职责 计算并返回新值 执行副作用(异步、DOM 操作等)
是否返回新数据 ✅ 是 ❌ 否
缓存 ✅ 有 ❌ 无
适用场景 一个数据受多个数据影响 一个数据影响多个数据

经典口诀

  • computed 擅长处理:一个数据受多个数据影响
  • watch 擅长处理:一个数据影响多个数据

选择建议

场景 推荐 原因
根据现有数据计算新数据 computed 这是 computed 的天然职责
数据变化时发起异步请求 watch computed 不支持异步
数据变化时操作 DOM watch computed 不应产生副作用
多个数据联动计算一个值 computed 依赖追踪自动管理
一个数据变化触发多个操作 watch 可以执行任意逻辑

5.4 综合决策流程图

复制代码
需要根据数据变化做点什么?
│
├─ 需要计算出一个新值?
│  │
│  ├─ 是 ──→ 会被多次访问吗?
│  │         │
│  │         ├─ 是 ──→ 用 computed ✅(有缓存)
│  │         └─ 否 ──→ 用 computed 或 methods
│  │
│  └─ 否 ──→ 需要执行异步操作或复杂副作用?
│            │
│            ├─ 是 ──→ 用 watch ✅
│            └─ 否 ──→ 用 methods(主动触发)
│
└─ 是用户主动触发的操作(点击、输入等)?
   │
   └─ 是 ──→ 用 methods ✅

六、注意事项与最佳实践

6.1 ⚠️ computed 中不能使用异步操作

计算属性必须是同步的 ,不能包含异步操作(如 setTimeoutPromiseasync/await):

javascript 复制代码
// ❌ 错误:computed 不能是异步的
const asyncComputed = computed(async () => {
  const res = await fetch('/api/data')
  return res.json()
})

// ✅ 正确:异步操作应该放在 watch 或生命周期钩子中
const data = ref(null)
watch(someId, async (newId) => {
  data.value = await fetchData(newId)
})

6.2 ⚠️ computed 中不要修改其他数据

计算属性应该只做计算,不产生副作用(不修改其他数据、不操作 DOM):

javascript 复制代码
// ❌ 错误:computed 中产生了副作用
const total = computed(() => {
  // 不应该在 computed 中修改其他数据
  someOtherValue.value = price.value * 2
  return price.value * quantity.value
})

// ✅ 正确:computed 只做纯计算
const total = computed(() => {
  return price.value * quantity.value
})

6.3 ⚠️ 避免在 computed 中访问不存在的属性

computed 中访问 ref 时必须使用 .value,访问 reactive 对象时直接使用属性名:

javascript 复制代码
const count = ref(0)
const state = reactive({ name: 'Vue' })

// ✅ 正确
const double = computed(() => count.value * 2)
const greeting = computed(() => `Hello, ${state.name}`)

// ❌ 错误:忘记 .value
const wrong = computed(() => count * 2)  // count 是对象,不是数字

6.4 ⚠️ 计算属性的依赖必须是响应式的

computed 只能追踪响应式数据的变化:

javascript 复制代码
// ❌ 错误:依赖的不是响应式数据
let plainNumber = 0
const badComputed = computed(() => plainNumber * 2)
// plainNumber 变化时,badComputed 不会更新

// ✅ 正确:依赖响应式数据
const reactiveNumber = ref(0)
const goodComputed = computed(() => reactiveNumber.value * 2)

6.5 ✅ 利用 computed 的链式依赖

计算属性可以相互依赖,Vue 会自动管理依赖链:

javascript 复制代码
const price = ref(100)
const quantity = ref(3)

const subtotal = computed(() => price.value * quantity.value)
const tax = computed(() => subtotal.value * 0.1)      // 依赖 subtotal
const total = computed(() => subtotal.value + tax.value) // 依赖 subtotal 和 tax

// 当 price 或 quantity 变化时,subtotal、tax、total 会自动按顺序更新

七、总结

Vue 2 vs Vue 3 computed 全景对比

对比维度 Vue 2(Options API) Vue 3(组合式 API)
定义方式 computed: { ... } 选项 computed() 函数
导入 无需导入 import { computed } from 'vue'
返回值类型 直接作为 this 属性 返回 ref 对象
模板访问 {``{ fullName }} {``{ fullName }}(自动解包)
Script 访问 this.fullName fullName.value
可写计算属性 { get() {}, set() {} } { get() {}, set() {} }
底层实现 Object.defineProperty + Watcher Proxy + Effect
缓存机制 基于 dirty 标志 基于 _dirty 标志 + 懒更新

核心要点回顾

  1. computed 是什么 :根据已有响应式数据派生新数据的工具
  2. 为什么要用 computed缓存 机制避免重复计算,声明式编程让代码更清晰
  3. computed vs methods:computed 有缓存,methods 没有;computed 自动更新,methods 需要调用
  4. computed vs watch :computed 用于派生数据 ,watch 用于执行副作用;computed 是"计算",watch 是"响应"
  5. 选择原则 :需要计算新值用 computed,需要执行异步操作用 watch,需要主动触发用 methods

理解 computed 的演进和正确用法,能让你在处理数据派生、表单联动、列表统计等场景时更加得心应手。记住:能用 computed 的地方就用 computed,它会让你的代码更高效、更清晰、更易维护

相关推荐
一孤程1 小时前
Airtest自动化测试第五篇:小程序与Web测试——跨平台自动化全覆盖
前端·自动化测试·小程序·自动化·测试·airtest
IT_陈寒1 小时前
SpringBoot自动配置不是你以为的那样的智能
前端·人工智能·后端
yume_sibai2 小时前
大屏数据可视化 - 边框红绿呼吸灯实现详解
前端·信息可视化·typescript
竹林8183 小时前
从 ethers.js 迁移到 Viem:一个签名验证 Bug 让我彻底放弃旧爱
javascript
Hyyy3 小时前
很多Desktop都在上的Computer Use是什么
前端·llm
慢功夫3 小时前
开篇:VS Code 为什么不是一个普通 React App
前端·visual studio code
你挚爱的强哥3 小时前
Vue2 实现 1.5s 十连击监听(连续点击若干次),封装通用可复用点击检测工具,不用 data!Vue 封装通用连击监听方法,支持自定义时长与点击次数
前端·javascript·vue.js
半个落月3 小时前
用 LangChain.js 手写一个能读写文件和执行命令的 Mini Cursor
javascript·人工智能·后端
天外天-亮4 小时前
HBuilder X 使用 uview-plus 方式
开发语言·javascript·hbuilder x·uview-plus
huashengzsj4 小时前
Shopify个人建站如何搭建独立站:从零开始的完整指南
前端·网络·人工智能