走进computed,了解computed的前世今生
computed(计算属性)并不是vue独创的,而是源自计算机科学和响应式编程的长期发展
计算理论的奠基:
- 函数式编程的纯函数思想:计算属性的核心特征(无副作用、依赖输入确定输出)直接来源于函数式编程中的纯函数概念
- 响应式编程的衍生值:在响应式系统中,任何可以依据其他的“可观察量”自动计算出的值都可以视为计算属性
vue中computed的演进
vue 1.x时代:(2014-2016)
初步实现:通过 computed 选项定义计算属性,基于依赖收集的响应式系统
特点:
- 基于Object.defineProperty实现响应式
- 计算属性会被混入到Vue实例中,像普通属性一样访问
- 已具备缓存机制,依赖不变时直接返回缓存值
Vue 2.x 时代:(2016-2020)
优化改进:
- 计算属性的惰性求值:只在真正被访问时才计算
- 更精细的依赖追踪:基于 Watcher 类的依赖收集系统
- 支持 setter:允许通过赋值反向影响依赖项3
computed: {fullName: {get() { return this.firstName + ' ' + this.lastName },set(newValue) { /* 处理赋值逻辑 */ }}
}
Vue 3.x 时代(2020至今)
组合式 API 重构:
- 引入 computed() 函数,可在 setup() 或 在 < script setup > 中使用
- 基于 Proxy 的响应式系统,解决 Vue 2 的检测限制
- 与 Reactivity System 深度集成,性能显著提升
import { computed } from 'vue'
const count = ref(1)
const double = computed(() => count.value * 2)
computed设计核心原理:
1、响应式依赖追踪
- 自动收集依赖:执行计算属性时候,访问的每个属性都会被记录依赖
- 更新触发:当任何属性变化时候,标记计算属性为“脏”dirty, 下次访问时候再重新计算
2、缓存机制的实现
// 简化的 computed 实现原理
function computed(getter) {let valuelet dirty = trueconst runner = effect(getter, {lazy: true,scheduler: () => {dirty = true // 依赖变化时标记为需要重新计算trigger(obj, 'value') // 触发依赖此计算属性的副作用}})const obj = {get value() {if (dirty) {value = runner() // 执行 getter 计算新值dirty = false}return value}}return obj
}
在vue中 同步派生状态指的是基于现有响应式数据立即计算得出的新状态,这种计算是同步完成的,具有即时性和准确性,这是computed计算属性的核心设计理念
同步派生状态的核心特征
1、即时计算:当依赖变化时候,派生值同步(立即)重新计算
const count = ref(1);
const double = computed(() => count.value * 2); // 同步计算
console.log(double.value); // 2
count.value = 3;
console.log(double.value); // 6 (立即更新)
2、纯函数特性:派生过程不允许修改外部状态,仅依赖输入:
// 纯派生:结果仅取决于 count.value
const triple = computed(() => count.value * 3);
3、与异步派生对比
特性 | 同步派生 (computed) | 异步派生 (如 watch + 状态) |
---|---|---|
执行时机 | 立即完成 | 延迟完成(需等待 Promise) |
模板引用 | 可直接使用 | 需要处理加载中/错误状态 |
响应式追踪 | 自动追踪依赖 | 需手动管理依赖 |
返回值 | 必须返回 | 无返回值(通过修改状态输出) |
需要每次执行的逻辑
1、模板渲染的即时性需求:vue模板渲染需要同步获取值进行渲染,异步派生会导致渲染中断或需要额外的处理加载状态
2、响应式系统的设计基础:vue的响应式依赖追踪依赖同步访问
3、缓存优化前提:同步计算使得缓存机制能精准判断是否需要重新计算
同步派生使用场景:
// 数据格式化
const price = ref(100);
const formattedPrice = computed(() => `¥${price.value.toFixed(2)}`);// 过滤/筛选列表
const todos = ref([/*...*/]);
const activeTodos = computed(() => todos.value.filter(todo => !todo.completed)
);// 多状态组合
const width = ref(10);
const height = ref(20);
const area = computed(() => width.value * height.value);
同步派生的优势:
- 性能高效:基于依赖追踪的精准缓存
- 心智模型简单:输入输出关系明确
- 调试友好:无隐藏的异步时序问题
- 组合方便:可安全地被其他计算属性引用
vue中的“同步派生状态”是通过computed来实现的即时性、确定性的数据转换,它是vue响应式系统的核心支柱,这种设计确保了:
- 模版渲染的即时性、可靠性
- 状态变化的可预测性
- 派生逻辑的高效性
computed为什么会有缓存机制:
vue中的computed采用缓存机制主要是为了优化性能和保证一致性:当计算属性依赖性没有变化时,vue会直接返回上一次的计算结果,而不会重新计算
缓存优势:
1、性能优化:
- 避免重复计算:模版中多次引用同一个计算属性时,依赖未变化就不需要重新计算;如果没有缓存,expensiveCalc 则会在每次渲染时候都需要计算
<template><div>{{ expensiveCalc }}</div> <!-- 计算一次 --><div>{{ expensiveCalc }}</div> <!-- 直接读缓存 -->
</template>
- 减少渲染开销: vue的渲染更新是批量的,缓存机制可以避免同一轮更新中多次触发相同的计算
2、保证一致性
- 同步派生状态的确定性:在同一个事件循环中,无论访问多少次计算属性,其结果都是相同
- 避免副作用污染:计算属性时纯函数,缓存机制强调开发者遵守这一原则, 无法在计算属性中执行副作用
3、与响应式系统的协同
- 精准依赖追踪:缓存机制依赖vue的响应式系统,只有当确定的依赖项变化时候才需要重新计算
缓存实现的关键技术
1、惰性计算:只有在真正访问属性时才会计算,如果依赖性没有变化则直接读取缓存值
2、标记-清除机制:“脏”(dirty)状态标记:当依赖性发生变化时,标记属性为“dirty”. 下次访问时候再重新计算
// 伪代码示意Vue内部逻辑
class ComputedRef {constructor(getter) {this._dirty = true; // 初始标记为需要计算this._value = undefined;}get value() {if (this._dirty) {this._value = getter(); // 重新计算this._dirty = false;}return this._value;}
}
3、依赖收集的精准性
- 计算属性执行时会自动追踪所有被访问的响应式变量
- 只有这些被追踪的变量变化时才会触发重新计算
何时会打破缓存:
- 依赖项发生变化(主动触发):
const a = ref(1);
const b = computed(() => a.value * 2);
a.value = 2; // 修改依赖,下次访问b时重新计算
- 手动强制刷新(特殊情况)
// 部分场景可通过赋值触发(不推荐)
b.value = 5; // 如果计算属性可写(setter)
与方法的对比:
特性 | computed (带缓存) | 方法 (无缓存) |
---|---|---|
执行时机 | 依赖变化时 | 每次调用都执行 |
性能 | 高效(缓存结果) | 可能重复计算 |
模板使用 | 自动优化 | 需自行优化(如 v-once) |
适用场景 | 纯计算、派生状态 | 需要每次执行的逻辑 |