深度解析:Vue中computed的实现原理(易懂不晦涩)

16 阅读7分钟

Vue中的computed(计算属性),核心作用是基于响应式数据派生新的响应式数据,兼具“缓存性”和“响应式”——只有依赖的响应式数据发生变化时,才会重新计算结果;否则直接复用缓存值,既减少无效计算、提升性能,又能简化模板中的复杂逻辑。其实现完全依赖Vue的响应式系统(依赖收集+依赖触发),核心逻辑可概括为:依赖收集(记录computed依赖的响应式数据)→ 缓存计算结果 → 依赖变化时重新计算并更新

一、先明确核心前提:Vue响应式的基础(极简回顾)

computed的实现依赖Vue响应式的核心机制——依赖收集与依赖触发,这是理解computed的关键,无需深入响应式源码,只需记住两个核心点:

  1. 响应式数据(如ref、reactive创建的数据)被访问时,会触发“依赖收集”:记录当前正在执行的“依赖函数”(比如computed的计算函数、watch的回调函数)。
  2. 响应式数据被修改时,会触发“依赖触发”:通知所有被记录的“依赖函数”重新执行,实现响应式更新。

简单来说:响应式数据就像“被监控的变量”,依赖函数就像“监控者”,数据变化时,会主动通知所有监控者“更新”。computed本质就是一个“特殊的监控者”,且自带“结果缓存”功能。

二、computed的核心实现逻辑(分3步拆解)

computed的实现核心是一个“computed watcher”(计算属性观察器),区别于普通的watcher(如watch的watcher),它多了“缓存机制”和“惰性求值”两个关键特性,具体实现分为3个步骤:

1. 初始化:创建computed watcher,开启“惰性求值”

当我们在Vue中定义一个computed时(无论是选项式API的computed选项,还是组合式API的computed函数),Vue会自动创建一个computed watcher,并传入computed的“计算函数”(比如() => a + b),同时设置一个核心标识:lazy: true(惰性求值)。

这里的“惰性求值”是computed的第一个关键特性:初始化时,不会立即执行计算函数,也不会主动收集依赖,只有当computed的值被访问时(比如在模板中使用、在代码中直接调用),才会触发第一次计算和依赖收集。

举个简单示例(组合式API):

<script setup>
import { ref, computed } from 'vue'
const a = ref(1)
const b = ref(2)
// 初始化computed,此时不会执行 () => a.value + b.value
const sum = computed(() => a.value + b.value)
</script>

2. 首次访问:执行计算函数,收集依赖并缓存结果

当computed的值被首次访问时(比如模板中使用{{ sum }},或代码中访问sum.value),会触发computed watcher的“求值逻辑”,具体做两件事:

  • 执行计算函数,得到计算结果:比如上述示例中,首次访问sum.value时,执行() => a.value + b.value,得到结果3。
  • 收集依赖,缓存结果:在执行计算函数的过程中,会访问a.value和b.value(响应式数据),此时会触发响应式数据的“依赖收集”,将当前的computed watcher记录到a和b的“依赖列表”中;同时,将计算结果(3)缓存到computed watcher的一个属性中(比如value),供后续访问复用。

核心细节:此时,a和b的依赖列表中,都包含了这个computed watcher——这意味着,当a或b发生变化时,会通知这个computed watcher“需要重新计算”。

3. 依赖变化:触发重新计算,更新缓存并通知后续依赖

当computed依赖的响应式数据(如a或b)被修改时,会触发“依赖触发”,执行以下流程:

  1. 响应式数据(如a.value = 3)被修改,触发自身的“依赖触发”逻辑,通知所有依赖它的watcher(包括当前的computed watcher)。

  2. computed watcher收到通知后,不会立即执行计算函数,而是先将自身标记为“dirty”(脏状态)——表示当前缓存的结果已经失效,需要重新计算。

  3. 当再次访问computed的值时,会检查computed watcher是否处于“dirty”状态:

    1. 若为“dirty”:执行计算函数(重新计算a.value + b.value,得到新结果),更新缓存,同时重置“dirty”状态为false,然后返回新的缓存结果。
    2. 若为“非dirty”:直接返回缓存的结果,不执行计算函数(这就是computed的缓存优势)。
  4. 若computed的值被其他watcher依赖(比如另一个computed、watch,或模板渲染),则会通知这些watcher,触发后续的更新(比如模板重新渲染)。

示例延伸:若修改a.value = 3,此时sum的computed watcher被标记为dirty;再次访问sum.value时,重新计算3 + 2 = 5,更新缓存并返回5;若再访问一次sum.value,因dirty为false,直接返回缓存的5,不重复计算。

三、computed的两个关键特性(实现的核心亮点)

1. 缓存性(核心优势)

缓存性是computed与普通函数的核心区别:普通函数每次被调用都会重新执行,而computed只有在依赖的响应式数据变化时,才会重新计算,否则复用缓存。

举个对比示例:

<script setup>
import { ref, computed } from 'vue'
const a = ref(1)
const b = ref(2)

// 普通函数:每次调用都重新计算
const getSum = () => a.value + b.value
// computed:依赖不变时,复用缓存
const sum = computed(() => a.value + b.value)

// 多次访问
console.log(getSum()) // 3(执行计算)
console.log(getSum()) // 3(再次执行计算)
console.log(sum.value) // 3(执行计算,缓存结果)
console.log(sum.value) // 3(复用缓存,不计算)
</script>

优势:当计算逻辑复杂(比如循环、复杂运算)时,缓存能大幅减少无效计算,提升页面性能——这也是为什么复杂逻辑推荐用computed,而非普通函数。

2. 惰性求值

computed的“惰性”体现在两个方面:

  • 初始化时不计算:只有首次访问computed的值,才会执行第一次计算。
  • 依赖变化时不立即计算:依赖数据变化后,仅标记为dirty,直到再次访问computed的值,才会重新计算——避免了“依赖变化就立即计算”的无效开销(比如依赖变化但computed的值并未被使用)。

四、computed与watch的核心区别(进一步理解实现)

很多开发者会混淆computed和watch,两者本质都是基于响应式系统实现,但定位和实现逻辑不同,核心区别如下(贴合实现原理):

对比维度computedwatch
核心定位派生新的响应式数据(计算结果)监听响应式数据变化,执行副作用(如接口请求、DOM操作)
实现核心computed watcher + 缓存 + 惰性求值普通watcher + 即时触发(或延迟触发),无缓存
触发时机依赖变化后,首次访问时触发计算依赖变化时立即触发(默认),或延迟触发(flush: 'post')
是否缓存有缓存,依赖不变则复用结果无缓存,每次依赖变化都执行回调

五、补充:computed的只读特性(实现层面说明)

默认情况下,computed是只读的——即不能直接修改computed的值(比如sum.value = 10会报错),这是因为computed的实现中,只暴露了“getter”(获取计算结果的方法),没有暴露“setter”(修改值的方法)。

若需要修改computed的值,需手动配置setter(Vue2/Vue3均支持),此时computed会在setter中修改依赖的响应式数据,间接更新自身的计算结果,示例:

<script setup>
import { ref, computed } from 'vue'
const a = ref(1)
const b = ref(2)

// 配置setter,可修改computed
const sum = computed({
  get: () => a.value + b.value,
  set: (newValue) => {
    // 修改依赖的响应式数据,间接更新sum
    a.value = newValue - b.value
  }
})

sum.value = 5 // 触发setter,a.value = 5 - 2 = 3,sum.value变为5
</script>

实现逻辑:setter的本质的是“修改computed依赖的响应式数据”,当依赖数据变化后,computed会被标记为dirty,下次访问时重新计算,实现“修改computed”的效果——本质还是依赖响应式系统的触发机制。

六、总结(一句话看懂实现)

computed的实现,本质是基于Vue的响应式依赖收集/触发机制,通过computed watcher实现“惰性求值+结果缓存” :依赖不变时复用缓存,依赖变化时标记脏状态,下次访问再重新计算,既保证响应式更新,又减少无效计算、提升性能。

简单类比:computed就像一个“智能计算器”,只有输入(依赖的响应式数据)变化,且有人需要输出(访问computed的值)时,才会重新计算,否则一直复用上次的计算结果,高效又省心。