深度解析 Vue 3 Computed 计算属性

技术 #Vue
浏览量:53
发布于 2026-08-10 · 更新于 2026-09-15
# 深度解析 Vue 3 Computed 计算属性:从响应式原理到高阶实战
​
在 Vue 3 开发中,`computed`(计算属性)是我们最频繁使用的 API 之一。然而,很多开发者仅停留在“用来拼接字符串”或“做简单数值计算”的基础用法上,对其背后的**缓存机制、惰性求值原理以及高阶可写用法**缺乏深层次的理解。
​
本文将从**核心本质、应用场景、高阶用法及避坑指南**全方位拆解 `computed`。
​
---
​
## 一、 核心本质:如何理解 Computed?
​
理解 `computed` 的关键在于一句话:**“它是基于响应式依赖进行缓存的派生状态。”**
​
拆解来看包含三个核心特征:
​
1. **派生性(Derived State)**:计算属性本身不直接存储独立数据,它的值是由其他响应式数据(`ref` / `reactive` / `props`)计算得出的。
2. **缓存性(Cached)**:这是它与普通 `methods` 函数的核心区别!只要依赖的响应式数据未发生改变,多次访问 `computed` 只会**直接返回上一次缓存的结果**,避免重复计算开销。
3. **惰性求值(Lazy Evaluation)**:当依赖的响应式数据改变时,`computed` **不会立刻重新计算**,而是仅将内部的“脏标记(dirty)”设为 `true`。直到下一次**视图渲染或代码显式读取**它时,才会真正触发重算并刷新缓存。
​
---
​
## 二、 基础与常见应用场景
​
### 1. 数据格式化与拼装
```vue
<script setup lang="ts">
import { ref, computed } from 'vue'
​
const firstName = ref('张')
const lastName = ref('三')
​
// 依靠缓存避免字符串重复拼接
const fullName = computed(() => `${firstName.value}${lastName.value}`)
</script>

2. 列表筛选与动态过滤

在不污染/修改原始列表的前提下,根据搜素关键字生成派生列表:

<script setup lang="ts">
import { ref, computed } from 'vue'
​
const searchKey = ref('')
const goodsList = ref([
  { id: 1, name: 'Apple iPhone' },
  { id: 2, name: 'Huawei Mate' },
  { id: 3, name: 'Xiaomi Phone' }
])
​
const filteredList = computed(() => {
  if (!searchKey.value) return goodsList.value
  return goodsList.value.filter(item => 
    item.name.toLowerCase().includes(searchKey.value.toLowerCase())
  )
})
</script>

3. 多依赖聚合统计(如购物车)

<script setup lang="ts">
import { ref, computed } from 'vue'
​
const cartItems = ref([
  { price: 100, count: 2, selected: true },
  { price: 200, count: 1, selected: false }
])
​
// 聚合计算总价与全选状态
const totalPrice = computed(() => {
  return cartItems.value
    .filter(item => item.selected)
    .reduce((sum, item) => sum + item.price * item.count, 0)
})
</script>

三、 高阶用法与进阶技巧

1. 可写计算属性(Writable Computed:get set

默认的 computed 是只读的。但在需要双向绑定(如 v-model改变计算属性需反向拆分/同步更新原始数据源时,可以使用带有 getset 的对象形式。

示例 A:表单双向绑定与反向拆分

<script setup lang="ts">
import { ref, computed } from 'vue'
​
const firstName = ref('John')
const lastName = ref('Doe')
​
const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue: string) {
    // 收到赋值时,反向更新原始数据源
    const [first, last] = newValue.split(' ')
    firstName.value = first || ''
    lastName.value = last || ''
  }
})
</script>
​
<template>
  <input v-model="fullName" />
</template>

示例 B:组件层层传递中 v-model 的二次封装代理

在封装 UI 组件时,利用 computedset 转发 emit,优雅实现属性中转:

<!-- CustomInput.vue -->
<script setup lang="ts">
import { computed } from 'vue'
​
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits(['update:modelValue'])
​
const value = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})
</script>
​
<template>
  <el-input v-model="value" />
</template>

2. 计算属性闭包传参

computed 本身不能直接接收参数。若在 v-for 循环中需要根据每项数据做计算,可以通过 computed 返回一个闭包函数来实现:

<script setup lang="ts">
import { ref, computed } from 'vue'
​
const userRoles = ref(['editor', 'author'])
​
// 返回一个闭包函数
const hasPermission = computed(() => {
  return (role: string) => userRoles.value.includes(role)
})
</script>
​
<template>
  <button v-if="hasPermission('editor')">编辑文章</button>
  <button v-if="hasPermission('admin')">删除文章</button>
</template>

⚠️ 注意:返回闭包函数时,内部函数在模板渲染调用时无法享受缓存,但外层仍能保留对依赖的追踪。


3. 计算属性调试(Debugging)

Vue 3 支持为 computed 传入选项参数,方便追踪依赖收集(onTrack)与更新触发(onTrigger):

const count = ref(0)
const double = computed(
  () => count.value * 2,
  {
    onTrack(e) {
      console.log('依赖追踪:', e.target)
    },
    onTrigger(e) {
      console.log('依赖更新:', e.target)
    }
  }
)

四、 避坑指南与最佳实践

1. 绝对不要在 get 中产生副作用(Side Effects)

computedget 函数必须是纯函数(Pure Function)

  • 严禁:在 get 内部修改其他响应式数据、发起 AJAX 异步请求或直接操作 DOM。这可能导致响应式追踪引发无限死循环。
  • 建议:任何异步操作或主动副作用,请使用 watchwatchEffect

2. 避免在 set 中编写无关的隐蔽逻辑

set 中修改数据源是完全合法的,但修改的目标应该与该 computed 在业务上有明确的因果/连带关系(如标志位更新、原始字段拆分),避免触发不相干模块的隐蔽变更。

3. 区分 computed watch

  • computed:用于根据旧数据计算导出新数据(同步、有返回值、有缓存)。
  • watch:用于监听数据变化并执行操作(可处理异步、无返回值、用于副作用处理)。

💡 总结思考决策树

在编写计算属性时,可以按照以下思路进行思考决策:

需要根据现有数据导出新状态?
 └── 否:使用普通变量 / 函数 / watch
 └── 是:使用 computed
      └── 是否频繁变化且多处使用? ➔ 享受缓存收益
      └── 是否需要配合 v-model / 逆向改源数据?
           ├── 否(90% 场景):使用只读函数签名 computed(() => ...)
           └── 是(10% 场景):使用开箱 get/set 对象签名 computed({ get, set })
评论

© 2026 Tarzan Blog. All rights reserved.