Vue 2 与 Vue 3 计算属性完全指南:从 Options API 到组合式 API 的演进
2026/7/11 1:04:22 网站建设 项目流程

前言

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

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

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

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

1.1 从模板中的表达式说起

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

<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平级。计算属性的值是一个函数,函数的返回值就是计算属性的值。

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

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

<template> <p>全名:{{ fullName }}</p> <p>总价:{{ totalPrice }}</p> <p>折后价:{{ discountPrice }}</p> </template>

2.2 可写计算属性:提供 setter

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

exportdefault{data(){return{firstName:'张',lastName:'三'}},computed:{fullName:{// getter:读取时触发get(){returnthis.firstName+' '+this.lastName},// setter:赋值时触发set(newVal){constnames=newVal.split(' ')this.firstName=names[0]this.lastName=names[names.length-1]}}}}
<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()函数

<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>
<template> <!-- 模板中自动解包,不需要 .value --> <p>全名:{{ fullName }}</p> <p>总价:{{ totalPrice }}</p> </template>

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

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

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

<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机制实现:

核心数据结构

// 简化版 ComputedRefImplclassComputedRefImpl{constructor(getter){this._dirty=true// 脏标记:是否需要重新计算this._value=undefined// 缓存的值// 创建 effect,scheduler 在依赖变化时触发this.effect=newReactiveEffect(getter,()=>{this._dirty=true// 依赖变化时标记为"脏"})}getvalue(){if(this._dirty){this._value=this.effect.run()// 仅在脏时重新计算this._dirty=false}returnthis._value}}

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

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

这种设计的优势在于:

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

脏标记(_dirty)机制

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

四、真实项目实战

4.1 场景一:购物车计算

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

Vue 3 实现

<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 场景二:表单联动计算

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

<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 场景三:待办事项的统计

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

<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 核心区别速览

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

5.2 computed vs methods

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

核心区别缓存

<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利用缓存,避免重复计算
需要传参进行计算methodscomputed不支持传参
计算开销大,但依赖变化不频繁computed缓存能显著提升性能
需要主动触发的逻辑(如点击事件)methods这是方法的天然职责

5.3 computed vs watch

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

核心区别

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

经典口诀

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

选择建议

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

5.4 综合决策流程图

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

六、注意事项与最佳实践

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

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

// ❌ 错误:computed 不能是异步的constasyncComputed=computed(async()=>{constres=awaitfetch('/api/data')returnres.json()})// ✅ 正确:异步操作应该放在 watch 或生命周期钩子中constdata=ref(null)watch(someId,async(newId)=>{data.value=awaitfetchData(newId)})

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

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

// ❌ 错误:computed 中产生了副作用consttotal=computed(()=>{// 不应该在 computed 中修改其他数据someOtherValue.value=price.value*2returnprice.value*quantity.value})// ✅ 正确:computed 只做纯计算consttotal=computed(()=>{returnprice.value*quantity.value})

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

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

constcount=ref(0)conststate=reactive({name:'Vue'})// ✅ 正确constdouble=computed(()=>count.value*2)constgreeting=computed(()=>`Hello,${state.name}`)// ❌ 错误:忘记 .valueconstwrong=computed(()=>count*2)// count 是对象,不是数字

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

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

// ❌ 错误:依赖的不是响应式数据letplainNumber=0constbadComputed=computed(()=>plainNumber*2)// plainNumber 变化时,badComputed 不会更新// ✅ 正确:依赖响应式数据constreactiveNumber=ref(0)constgoodComputed=computed(()=>reactiveNumber.value*2)

6.5 ✅ 利用 computed 的链式依赖

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

constprice=ref(100)constquantity=ref(3)constsubtotal=computed(()=>price.value*quantity.value)consttax=computed(()=>subtotal.value*0.1)// 依赖 subtotalconsttotal=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.fullNamefullName.value
可写计算属性{ get() {}, set() {} }{ get() {}, set() {} }
底层实现Object.defineProperty+ WatcherProxy+ 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,它会让你的代码更高效、更清晰、更易维护

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询