Vue3计算属性

Vue 3 计算属性完全指南

本文档从零开始讲解 Vue 3 的计算属性(Computed):是什么、为什么用、和方法的区别、只读与可写、常见场景和易错点,配有大量示例,适合新手系统学习。


一、什么是计算属性?

1.1 生活中的类比

有时你需要根据已有数据“算出来”一个新结果,例如:

  • 根据姓和名得到“全名”
  • 根据购物车列表得到“总价”
  • 根据列表和筛选条件得到“筛选后的列表”

在 Vue 里,这种由其它响应式数据推导出来的值,可以用 计算属性 来表示。
你只要写好“怎么算”,Vue 会自动在依赖变化时重新计算,并像普通数据一样在模板里使用。

1.2 计算属性长什么样?

计算属性在 <script setup> 里用 computed() 定义:
你传入一个函数,这个函数返回一个值,这个值就是“计算出来的结果”。
在模板里使用时,和 ref 一样不用写 .value;在 script 里要用 .value 才能拿到值。

<template>
  <div>
    <p>全名:{{ fullName }}</p>
  </div>
</template>

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

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

const fullName = computed(() => {
  return firstName.value + lastName.value
})
</script>
  • fullName 是计算属性:由 firstNamelastName 推导出来的。
  • firstNamelastName 变化时,fullName自动重新计算
  • 模板里直接写 {{ fullName }},不需要 fullName.value

二、为什么用计算属性?和“方法”有什么区别?

2.1 用方法也能“算”出来

下面用方法实现“全名”:

<template>
  <div>
    <p>全名:{{ getFullName() }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
function getFullName() {
  return firstName.value + lastName.value
}
</script>

效果看起来一样,但有一个重要区别:方法每次渲染都会重新执行,而计算属性会缓存结果

2.2 计算属性会“缓存”,方法不会

  • 计算属性:只有它依赖的数据(如上面的 firstNamelastName)变化时,才会重新计算;否则一直用上一次的结果(缓存)。
  • 方法:每次模板重新渲染(例如其它数据变了),都会再执行一遍。

所以:

  • 需要根据某些数据推导出一个值,并且希望依赖没变就不重复算 → 用 computed
  • 需要执行一段逻辑(比如点击时调用)、不强调“缓存一个结果” → 用 方法

2.3 简单对比表

对比项 计算属性 computed 方法 methods
写法 computed(() => ...) function fn() { }
模板里使用 {{ fullName }} {{ getFullName() }}
是否缓存 是,依赖不变不重算 否,每次渲染都可能执行
适用 派生数据、过滤、合计 事件处理、不依赖“缓存”的逻辑

三、基本语法

3.1 只读计算属性(最常用)

语法: const 变量 = computed(() => { return 某个值 })

  • 传入一个函数(getter),返回值就是计算属性的值。
  • 这个函数里用到的 ref/reactive 会被 Vue 自动收集为“依赖”;依赖变了,计算属性会重新算。
<template>
  <div>
    <p>单价:{{ price }},数量:{{ count }}</p>
    <p>总价:{{ total }}</p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
const price = ref(10)
const count = ref(3)
const total = computed(() => {
  return price.value * count.value
})
</script>
  • total 依赖 pricecount;任一个变化,total 会重新计算。
  • script 里若要用 total 的值,要写 total.value;在 template 里直接写 total 即可。

3.2 计算属性在 script 里要写 .value

计算属性返回的也是一个 ref(只读),所以在 <script setup> 里读取时要 .value

<script setup>
import { ref, computed } from 'vue'
const count = ref(2)
const double = computed(() => count.value * 2)

console.log(double.value)  // 4
function printDouble() {
  alert(double.value)
}
</script>

模板里不需要:

<template>
  <p>{{ double }}</p>
</template>

四、可写计算属性(getter + setter)

默认情况下,计算属性是只读的:你不能写 fullName.value = '李四'
若你希望“从外面给计算属性赋值时,反过来更新它依赖的源数据”,可以写成 get + set 的形式。

4.1 语法

传入一个对象,包含 getset 两个函数:

const 变量 = computed({
  get() {
    return 根据其它数据算出的值
  },
  set(newValue) {
    // 根据 newValue 去更新依赖的源数据
  }
})

4.2 示例:全名可读可写

“全名”由“姓”和“名”组成;当用户直接改“全名”时,自动拆成“姓”和“名”写回去。

<template>
  <div>
    <p>姓:<input v-model="firstName" /></p>
    <p>名:<input v-model="lastName" /></p>
    <p>全名:<input v-model="fullName" /></p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')

const fullName = computed({
  get() {
    return firstName.value + lastName.value
  },
  set(newValue) {
    const len = newValue.length
    if (len >= 2) {
      firstName.value = newValue[0]
      lastName.value = newValue.slice(1)
    } else if (len === 1) {
      firstName.value = newValue
      lastName.value = ''
    } else {
      firstName.value = ''
      lastName.value = ''
    }
  }
})
</script>
  • get:读 fullName 时,返回 firstName + lastName
  • set:写 fullName 时(例如在 input 里改全名),把新值拆成姓和名,更新 firstNamelastName

可写计算属性用得相对少,了解即可;大部分场景只读就够用。


五、常见使用场景

5.1 由多个字段组合成一个显示值

<template>
  <p>完整地址:{{ fullAddress }}</p>
</template>

<script setup>
import { ref, computed } from 'vue'
const province = ref('广东省')
const city = ref('深圳市')
const district = ref('南山区')
const fullAddress = computed(() => {
  return province.value + city.value + district.value
})
</script>

5.2 列表过滤(根据关键词筛选)

<template>
  <div>
    <input v-model="keyword" placeholder="搜索" />
    <ul>
      <li v-for="item in filteredList" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
const keyword = ref('')
const list = ref([
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' },
  { id: 3, name: '橙子' }
])
const filteredList = computed(() => {
  const k = keyword.value.trim().toLowerCase()
  if (!k) return list.value
  return list.value.filter((item) => item.name.toLowerCase().includes(k))
})
</script>
  • filteredList 依赖 keywordlist;任一变化都会重新过滤。
  • 模板里用 v-for="item in filteredList",无需在模板里写过滤逻辑,也更利于配合 v-for:key

5.3 列表排序(不改变原数组)

<template>
  <ul>
    <li v-for="item in sortedList" :key="item.id">
      {{ item.name }} - {{ item.score }} 分
    </li>
  </ul>
</template>

<script setup>
import { ref, computed } from 'vue'
const list = ref([
  { id: 1, name: '小明', score: 85 },
  { id: 2, name: '小红', score: 92 },
  { id: 3, name: '小刚', score: 78 }
])
const sortedList = computed(() => {
  return [...list.value].sort((a, b) => b.score - a.score)
})
</script>

[...list.value] 先拷贝再排序,不修改原数组,符合“计算属性不产生副作用”的习惯。

5.4 合计、统计

<template>
  <div>
    <p>总价:¥{{ totalPrice }}</p>
    <p>数量:{{ totalCount }} 件</p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
const cart = ref([
  { id: 1, name: '苹果', price: 5, count: 2 },
  { id: 2, name: '香蕉', price: 3, count: 5 }
])
const totalPrice = computed(() => {
  return cart.value.reduce((sum, item) => sum + item.price * item.count, 0)
})
const totalCount = computed(() => {
  return cart.value.reduce((sum, item) => sum + item.count, 0)
})
</script>

5.5 布尔类“是否……”判断

<template>
  <div>
    <button :disabled="!canSubmit">提交</button>
    <p v-if="isEmpty">列表为空</p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
const list = ref([])
const form = ref({ name: '', age: 0 })

const isEmpty = computed(() => list.value.length === 0)
const canSubmit = computed(() => {
  return form.value.name.trim() !== '' && form.value.age > 0
})
</script>

5.6 格式化显示(日期、金额等)

<template>
  <p>下单时间:{{ formattedDate }}</p>
  <p>金额:{{ formattedPrice }}</p>
</template>

<script setup>
import { ref, computed } from 'vue'
const orderDate = ref('2025-02-24')
const price = ref(1234.5)

const formattedDate = computed(() => {
  return orderDate.value.replace(/-/g, '/')
})
const formattedPrice = computed(() => {
  return '¥' + price.value.toFixed(2)
})
</script>

复杂格式化可以再拆成方法或工具函数,在 computed 里调用即可。

5.7 计算属性依赖另一个计算属性

计算属性可以依赖别的 ref计算属性,形成“链式推导”:

<template>
  <p>总价(含税):{{ totalWithTax }}</p>
</template>

<script setup>
import { ref, computed } from 'vue'
const cart = ref([
  { price: 10, count: 2 },
  { price: 5, count: 3 }
])
const total = computed(() => {
  return cart.value.reduce((sum, item) => sum + item.price * item.count, 0)
})
const taxRate = ref(0.1)
const totalWithTax = computed(() => {
  return (total.value * (1 + taxRate.value)).toFixed(2)
})
</script>
  • totalWithTax 依赖 totaltaxRate;total 本身是计算属性,依赖 cart。
  • 只要 cart 或 taxRate 变化,整条链都会按需重新计算。

六、注意点与易错点

6.1 不要在计算属性里改其它数据(避免副作用)

计算属性应该是纯函数:根据依赖算出结果,不要在里面写 赋值、请求接口、DOM 操作 等。
否则依赖关系会乱,缓存和更新时机也可能不符合预期。

// 不推荐
const bad = computed(() => {
  count.value++  // 不要在里面改别的数据
  return count.value
})

需要“算完再干别的事”,放在 watch事件处理函数 里更合适。

6.2 计算属性要“返回”一个值

getter 函数必须 return 一个值,这个值才会成为计算属性的结果。
若忘记 return,计算属性会是 undefined

const wrong = computed(() => {
  const x = a.value + b.value
  // 忘记 return,wrong 一直是 undefined
})
const right = computed(() => {
  return a.value + b.value
})

6.3 依赖要在“读取时”被访问到

Vue 通过在运行 getter 时记录用到了哪些 ref/reactive 来收集依赖。
若你把依赖写在条件里,某次执行没走到,那一次就不会被当作依赖,可能导致“该更新时没更新”:

// 若 condition 为 false,someRef 可能不会被收集为依赖
const risky = computed(() => {
  if (condition.value) {
    return someRef.value
  }
  return 0
})

尽量让计算属性用到的响应式数据在每次 getter 执行时都能被访问到(至少在同一逻辑分支里稳定出现)。

6.4 不要给只读计算属性赋值

没有写 set 的计算属性是只读的,不能 xxx.value = 新值,否则会报错。
需要“可写”时,用 get/set 形式的 computed。


七、计算属性与 ref / reactive 对比

对比项 计算属性 computed ref / reactive
来源 由其它数据推导 直接定义的源数据
是否可写 默认只读,可配 set 可写
是否缓存 不涉及“重算”
典型用途 派生状态、过滤、合计 原始状态、用户输入

简单记:“算出来的”用 computed,“直接存的”用 ref/reactive


八、速查表与学习建议

8.1 速查表

需求 写法
只读计算属性 const x = computed(() => 返回值)
可写计算属性 computed({ get() { return ... }, set(v) { ... } })
模板里使用 直接 {{ x }},不用 .value
script 里使用 x.value
依赖 ref/reactive 在 getter 里正常 .value 或访问属性即可,Vue 自动收集依赖

8.2 学习建议

  1. 先掌握只读computed(() => ...),能根据列表、表单等算出“过滤结果、总价、是否可提交”等。
  2. 理解缓存:依赖不变就不重算,适合派生数据。
  3. 不在计算属性里写副作用;需要“算完再请求/再改别的”时用 watch 或方法。
  4. 可写计算属性(get/set)在需要“双向派生”时再用(如全名 ↔ 姓+名)。

把本文档里的示例在项目里敲一遍、改依赖数据看结果是否自动更新,会掌握得更牢。祝你学习顺利。

发表评论