3. computed计算属性:从OptionsAPI的computed到组合式API的computed函数

计算属性,说白了就是「有缓存的计算结果」。

我记得刚接触Vue时,最让我眼前一亮的就是computed。你想想看,不用手动管理依赖,不用操心什么时候重新计算,它自己就搞定了。但在OptionsAPI时代,computed的写法其实有点「死板」——你只能声明一个名字,然后给它一个getter。到了组合式API,computed变成了一个函数,灵活度直接拉满。

这一节,我们就来聊聊computed的迁移。我会从最简单的只读计算属性讲起,再到可写的计算属性,最后分享一些我在项目中踩过的坑。

3.1 从OptionsAPI到组合式API:基础迁移

先看一个最典型的场景:我们有一个用户对象,需要展示「全名」。

OptionsAPI写法:

export default {
  data() {
    return {
      firstName: '张',
      lastName: '三'
    }
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName
    }
  }
}

组合式API写法:

import { ref, computed } from 'vue'

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

const fullName = computed(() => {
  return firstName.value + ' ' + lastName.value
})

嗯,这里要注意一个细节:在组合式API中,computed接收的是一个函数,而不是一个对象。这个函数就是getter,它必须返回一个值。

我个人习惯把computed放在ref后面,这样代码读起来很顺畅——先声明数据源,再声明派生数据。你想想看,这不就是数据流的自然顺序吗?

3.2 只读计算属性:默认行为

上面那个例子,其实就是只读计算属性。为什么叫「只读」?因为你不能直接修改fullName的值。

试试看:

fullName.value = '李四' // ❌ 报错!Computed is readonly

为什么会这样?因为computed默认只暴露getter,没有setter。它的值完全由依赖的响应式数据决定。你强行赋值,Vue会告诉你「不行,这是只读的」。

我在项目中遇到过一位同事,他试图在组件里直接修改计算属性的值来实现「双向绑定」,结果折腾了半天没搞明白。其实,计算属性的本质是派生状态,它的值应该由源数据推导而来,而不是被手动设置。

小技巧: 如果你只是想「重置」计算属性,正确的做法是修改它的依赖源,而不是修改计算属性本身。

3.3 可写计算属性:什么时候需要?

那有没有需要「写」计算属性的场景呢?有,而且不少。

最常见的场景就是v-model绑定一个需要转换的值。比如,你有一个价格输入框,用户输入的是「元」,但后端存储的是「分」。

看代码:

import { ref, computed } from 'vue'

const priceInFen = ref(100) // 后端存储的是分

// 可写计算属性
const priceInYuan = computed({
  // getter:从分转换为元
  get() {
    return priceInFen.value / 100
  },
  // setter:从元转换为分
  set(val) {
    priceInFen.value = val * 100
  }
})

现在,你可以直接在模板里用v-model绑定priceInYuan:

<input v-model="priceInYuan" type="number" />

用户输入10,priceInFen自动变成1000。用户看到的是元,后端存的是分,完美解耦。

核心要点: 可写计算属性不是让你「随意修改」,而是让你在修改时执行一些「转换逻辑」。setter里一定要修改依赖的源数据,否则计算属性不会更新。

3.4 可写计算属性的避坑指南

我曾经在一个电商项目中,用可写计算属性来处理购物车的「数量」和「小计」。结果出了个bug:用户修改数量后,小计没有立即更新。

排查了半天,发现是setter里写错了:

// ❌ 错误写法
const subtotal = computed({
  get() {
    return quantity.value * price.value
  },
  set(val) {
    // 这里只修改了quantity,但price没变
    quantity.value = val / price.value
  }
})

问题出在哪?如果price.value是0,直接报错。而且这种写法逻辑上也不对——小计应该是「数量×单价」,你修改小计时,到底是想改数量还是改单价?

我的建议是: 可写计算属性的setter,最好只修改一个依赖源。如果涉及多个依赖,很容易出现逻辑混乱。

场景 推荐做法 说明
单位转换(元↔分) 可写计算属性 setter只修改一个源数据,逻辑清晰
全名(姓+名) 只读计算属性 修改全名时,你不知道该改姓还是改名
购物车小计 只读计算属性 小计是派生值,不应该被手动设置
表单双向绑定+格式化 可写计算属性 getter格式化显示,setter解析存储

3.5 组合式API中computed的额外优势

除了读写控制,组合式API的computed还有一个隐藏技能——可以脱离组件使用

在OptionsAPI里,computed必须写在组件选项里。但在组合式API中,你可以把computed定义在独立的文件里,然后到处复用:

// utils/usePrice.js
import { ref, computed } from 'vue'

export function usePrice(initialFen = 0) {
  const priceInFen = ref(initialFen)

  const priceInYuan = computed({
    get: () => priceInFen.value / 100,
    set: (val) => { priceInFen.value = val * 100 }
  })

  return {
    priceInFen,
    priceInYuan
  }
}

然后在任何组件里:

const { priceInFen, priceInYuan } = usePrice(500)

你看,计算属性变成了一个「可组合的函数」。这种复用方式,在OptionsAPI里根本做不到。

注意: 虽然computed可以脱离组件,但它仍然依赖响应式系统。如果你在非Vue环境(比如纯JS文件)里使用computed,需要先调用effectScope或手动创建响应式上下文。不过99%的场景你不需要操心这个。

3.6 总结:迁移要点

最后,我整理了一个对照表,方便你快速迁移:

功能 OptionsAPI 组合式API
只读计算属性 computed: { fullName() { ... } } const fullName = computed(() => ...)
可写计算属性 computed: { fullName: { get, set } } const fullName = computed({ get, set })
依赖追踪 自动(通过this) 自动(通过响应式引用)
复用性 只能在组件内 可以提取到独立函数

说白了,computed的迁移是最平滑的。你只需要记住:OptionsAPI里用对象声明,组合式API里用函数调用。至于只读和可写,区别就在于传入的是函数还是对象。

下一节,我们会聊聊watch——那个比computed更「主动」的家伙。准备好了吗?