Vue3 computed 全解析:惰性执行、缓存机制与 getter/setter 实战

0 阅读8分钟

在Vue3响应式体系中,computed(计算属性)是处理“依赖推导值”的核心API,其设计初衷是在保证响应式联动的同时,通过惰性执行缓存机制优化性能,同时支持getter/setter双写法适配只读、可写场景。相比普通方法和effect副作用函数,computed更贴合“数据推导”的语义,也是日常开发中提升代码效率与性能的关键工具。本文将从特性原理、写法实战、避坑要点三个维度,彻底讲透computed的核心逻辑与最佳实践。

一、核心特性:惰性执行与缓存机制的协同逻辑

computed的两大核心特性——惰性执行和缓存机制,并非孤立存在,而是协同工作以实现“按需计算、复用结果”的目标,这也是它区别于组件方法、effect的核心优势。

1. 惰性执行:不访问不计算,拒绝无意义开销

computed的“惰性”体现在两个关键节点,彻底规避无效计算:

  • 初始化时不执行:创建computed对象时,其内部的计算逻辑(getter函数)不会立即执行,仅在首次访问.value时才触发第一次计算。这与effect不同——effect在初始化时会主动执行一次,若计算逻辑复杂,可能造成初始化性能损耗。
  • 依赖变化时不立即执行:当computed依赖的响应式数据发生变化时,它不会同步重新计算,仅会标记为“脏数据(dirty)”;只有当再次访问.value时,才会触发重新计算并更新结果。

通俗类比:computed就像一位“按需工作的计算器”,不会主动提前计算结果,也不会在数据源变动后立刻重新计算,只有当你主动索取结果(访问.value)时,才会根据数据源是否变化,决定是返回缓存结果还是重新计算。

2. 缓存机制:依赖不变,结果复用

缓存机制是computed性能优化的核心,其逻辑围绕“脏数据标记”展开,流程如下:

  1. 首次计算缓存:首次访问computed.value时,执行getter函数计算结果,将结果缓存到内部变量中,并标记为“非脏数据(!dirty)”。
  2. 依赖不变复用缓存:后续多次访问computed.value时,若依赖的响应式数据未变化(仍为!dirty),则直接返回缓存结果,不执行getter函数,避免重复计算。
  3. 依赖变化标记脏数据:当依赖的响应式数据发生变化时,computed通过响应式系统的trigger机制,将自身标记为“脏数据(dirty)”,但不立即计算。
  4. 再次访问重新计算:当标记为dirty后再次访问computed.value时,重新执行getter函数计算新结果,更新缓存,并重置为!dirty状态。
import { ref, computed } from 'vue';

const a = ref(1);
const b = ref(2);

// 定义computed,依赖a和b
const sum = computed(() => {
  console.log('执行计算逻辑');
  return a.value + b.value;
});

// 首次访问:执行计算,缓存结果(sum=3)
console.log(sum.value); // 输出:执行计算逻辑 → 3
// 再次访问:依赖未变,复用缓存
console.log(sum.value); // 输出:3(无计算日志)

// 修改依赖,标记为脏数据
a.value = 2;
// 未访问.value,不执行计算
console.log('依赖已修改,但未访问sum'); 
// 再次访问:依赖变化,重新计算(sum=4)
console.log(sum.value); // 输出:执行计算逻辑 → 4

缓存的价值:对于计算逻辑复杂(如循环遍历、数据转换)、访问频繁的场景,缓存机制能大幅减少计算开销,提升组件渲染性能——这是普通方法无法实现的(普通方法每次调用都会重新执行)。

3. 与组件方法、effect的核心区别

为更清晰理解computed的特性,对比三者的核心差异:

特性computed组件方法effect
执行时机惰性执行(访问.value时)调用时执行(每次调用都重新计算)初始化执行,依赖变化同步执行
缓存机制有(依赖不变复用结果)无(无缓存,重复调用重复计算)无(依赖变化必执行副作用)
核心用途响应式数据推导(只读/可写)通用逻辑封装(非响应式推导)响应式副作用处理(DOM操作、异步请求)

二、实战写法:getter/setter 适配不同场景

computed支持两种写法:只读写法(仅getter)可写写法(getter+setter) ,分别适配“仅推导值,不允许修改”和“既推导值,又允许手动修改”的场景。

1. 只读写法(默认):仅传递getter函数

这是最常用的写法,适用于仅需要通过依赖推导值,不允许手动修改computed结果的场景。此时computed返回一个只读的Ref对象,修改其.value会被拦截(开发环境抛出警告)。

import { ref, computed } from 'vue';

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

// 只读computed:推导全名
const fullName = computed(() => {
  return `${firstName.value}${lastName.value}`;
});

console.log(fullName.value); // 输出:张三
// 尝试修改只读computed(开发环境抛警告,修改无效)
fullName.value = '李四'; 

适用场景:模板渲染依赖的推导值(如拼接字符串、计算列表总数、格式化日期)、组件内部的只读数据推导等。

2. 可写写法:传递getter+setter对象

当需要手动修改computed结果,并同步更新依赖的响应式数据时,可传递一个包含getset方法的对象,实现可写computed。

  • get方法:与只读写法一致,负责根据依赖推导computed值。
  • set方法:接收手动修改的值,负责反向更新依赖的响应式数据,实现“修改computed值 → 同步更新数据源”的联动。
import { ref, computed } from 'vue';

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

// 可写computed:get推导值,set反向更新数据源
const fullName = computed({
  // get:推导全名
  get() {
    return `${firstName.value}${lastName.value}`;
  },
  // set:接收手动修改的fullName,拆分后更新firstName和lastName
  set(newFullName) {
    const [newFirst, newLast] = newFullName.split('');
    firstName.value = newFirst;
    lastName.value = newLast;
  }
});

console.log(fullName.value); // 输出:张三
// 手动修改computed值,触发set方法
fullName.value = '李四';
// 依赖的数据源同步更新
console.log(firstName.value); // 输出:李
console.log(lastName.value); // 输出:四
// get方法重新推导,结果同步
console.log(fullName.value); // 输出:李四

适用场景:需要双向绑定的计算属性(如表单中的组合值、可编辑的推导字段),手动修改computed值时,需同步更新底层数据源的场景。

注意:可写computed的核心是“反向更新数据源”,set方法中必须修改依赖的响应式数据,否则computed值会与数据源不一致,导致响应式异常。

三、避坑指南:这些细节决定computed使用效果

1. 避免在computed中执行副作用

computed的设计语义是“数据推导”,而非“副作用处理”。若在getter函数中执行DOM操作、异步请求、修改其他响应式数据等副作用,会导致:

  • 副作用执行时机不可控(因惰性执行,依赖变化后不立即执行);
  • 重复执行或遗漏执行(因缓存机制,依赖不变时不执行getter);
  • 代码逻辑混乱,难以调试。

正确做法:副作用逻辑应放在effect、watch或组件方法中,computed仅负责纯数据推导。

2. 依赖必须是响应式数据

computed的缓存和响应式更新,依赖于对响应式数据的追踪(track/trigger)。若getter函数中依赖的是非响应式数据,computed将无法感知数据变化,始终返回缓存结果,导致响应式失效。

import { computed } from 'vue';

// 非响应式数据
let a = 1;
const sum = computed(() => a + 2);

console.log(sum.value); // 输出:3
// 修改非响应式数据,computed无法感知,仍返回缓存
a = 2;
console.log(sum.value); // 输出:3(响应式失效)

3. 可写computed避免循环依赖

在可写computed的set方法中,若直接修改自身的.value,会触发get方法重新计算,进而可能再次触发set,形成循环依赖,导致栈溢出。

// 错误示例:循环依赖
const num = ref(1);
const wrongComputed = computed({
  get() {
    return num.value * 2;
  },
  set(newVal) {
    // 直接修改自身,触发循环
    wrongComputed.value = newVal / 2; 
  }
});

正确做法:set方法中仅修改依赖的底层响应式数据,不直接操作computed自身。

4. 复杂计算优先用computed

对于模板中频繁访问的复杂计算逻辑,优先使用computed而非组件方法——组件方法每次模板渲染都会重新执行,而computed通过缓存机制仅在依赖变化时重新计算,能显著提升渲染性能。

四、总结

computed的核心价值的是“高效的响应式数据推导”:通过惰性执行避免无意义的提前计算,通过缓存机制减少重复计算开销,同时借助getter/setter双写法适配只读、可写全场景。

使用computed的关键原则:坚守“数据推导”的语义,不混入副作用,依赖响应式数据,可写场景中确保反向更新底层数据源。掌握这些逻辑,能让computed成为优化代码性能、提升开发效率的利器,在复杂项目中发挥更大价值。