Vue3 响应式系统——computed 和 watch

学过上一节 ref、reactive、effect 后,接下来我们探究响应式变量的使用——computed 和 watch 。

一、computed 和 watch 概述

所有响应式逻辑都会依赖 effect 执行,computed / watch / render 本质都是 effect。

  • effect:依赖 state。
  • dep:被外部 effect 依赖。
  • computed​:带缓存的、惰性的、基于依赖的派生 effect
  • watch​:主动监听数据变化的副作用调度器

一句话总结二者的由来:

副作用(watch)不可缓存,派生值(computed)必须缓存

  • computed: _dirty + scheduler
  • watch / watchEffect: 直接执行副作用

二、computed 的底层实现

2.1 computed 的核心结构

源码中 computed 返回的是一个 ​ComputedRefImpl 实例​:

class ComputedRefImpl {
  _value
  dep
  effect
  _dirty = true
}

可以理解为:computed = ref + effect + dirty 标记

2.2 computed 的创建流程

function computed(getter) {
  const cRef = new ComputedRefImpl(getter)
  return cRef
}

构造函数内部核心逻辑(简化):

this.effect = new ReactiveEffect(getter, () => {
  () => return getter(this._value), // getter,本质上就是我们 computed 调用是传递的 fn 参数
  () => { // setter
    if (!this._dirty) {
      this._dirty = true
      triggerRefValue(this)
    }
  }
})

🚨 ​computed 自己不会重新计算,​它只会在「依赖变了(computedEffect.scheduler)」时,把 _dirty 标记为 true

而真正重新计算是在 effect() 时,触发了响应式变量的 getter(并且为脏数据),然后才计算 computedEffect.run() 。

2.3 computed.value 的 getter

get value() {
  trackRefValue(this)

  if (this._dirty) {
    this._dirty = false
    this._value = this.effect.run()
  }

  return this._value
}

2.3.1 依赖收集的是「computed 本身」

trackRefValue(this)

依赖首先收集的不是 track getter 里的响应式数据,而是:“谁用到了这个 computed”

2.3.2 computed 是惰性执行的

if (this._dirty) {
  this._value = this.effect.run()
}
  • 依赖变了不会立刻重新计算
  • 只有 ​.value​ 被访问时才重新算

2.3.3 computed 一定有缓存

this._dirty = false
return this._value

只要依赖没变,多次访问 computedValue.valuegetter 只执行一次。

2.4 computed 的完整执行链路

state.a 改变
   ↓
computed.effect.scheduler 执行
   ↓
_dirty = true(不会立刻算)
   ↓
下一次访问 computed.value
   ↓
effect.run() → 重新计算(“被动”计算)

2.5 computed 结合响应式变量的执行过程

2.5.1 过程概述

const state = reactive({ a: 1 })

const c = computed(() => state.a + 1)

effect(() => {
  console.log(c.value)
})
state.a++
 ↓
trigger state.a.dep
 ↓
computed.effect.scheduler
 ↓
computed._dirty = true
 ↓
trigger computed.dep
 ↓
render effect run
 ↓
computed.value 被访问
 ↓
computed.effect.run()  ← 真正计算

看似很复杂,其实就是一个 副作用收集触发的嵌套逻辑。

​🫡一句话总结:​全局 effect 执行,收集依赖“响应式变量”(computed 变量 + ref/reactive 变量),然后 computed 响应式变量又依赖于 ref/reactive 变量,把他们收集到 computed 变量自身的 effect 中。然后一旦 ref/reactive 变量更新,那么首先触发自身 trigger 更新,然后被依赖的 computed effect 的 trigger 更新,进而最终的 computed 变量更新(“懒更新”:用到的时候才 run)。

2.5.2 过程讲解

computed 和 ref/reactive 实例初始化过程之前已经详细讲解过,这里不再重复讲解。

computed 依赖收集过程:

  1. effect 执行,访问 computed.value
effect
   ↓
computed.dep.add(effect)
  1. computed.effect.run(),触发 computed 的 getter,进而触发 state(.a) 的 getter
state.a
  ↓
computed.effect
  ↓
computed.dep
  ↓
effect
  1. state.a 改变,触发 state.a 的 trigger
trigger(target, 'a')
=>  dep = state.a.dep
=>  computed.effect
  1. 执行 computed.effect.scheduler(此时并不是 run!)

内部执行 scheduler 的时候,之后会回头执行 _effect.run() ,也就相当于执行了 fn() 。

scheduler = () => {
  if (!this._dirty) {
    this._dirty = true // 打标记
    triggerRefValue(this) // 非 lazy 下才能在这里内部执行 run
  }
}
  1. triggerRefValue(this) 等价于
computed.dep.forEach(effect => effect.run()) // 通知“谁依赖 computed”
  1. render effect 重新执行,再次访问 computed.value
effect(() => {
  console.log(c.value)
})
// 然后才访问 computed.value
if (this._dirty) {
  this._value = this.effect.run() // computed 这时候才真正重新计算
}

2.5.3 误区解答

  1. 为什么 computed 不直接 run?
❌ 如果 state 改一次,computed 立刻算一次:
多个 state 连续变更 → 重复计算
computed 可能根本没人用
✅ 延迟到 .value 访问:
惰性
合并更新
性能最优
  1. 为什么 computed 需要自己的 dep?
effect(() => c.value)
如果 computed 没有 dep:
外部 effect 无法被触发
computed 更新无法传播
computed 本身就是一个“可依赖对象”

2.6 computed 为什么不直接做副作用?

computed(() => {
  console.log(state.count)
})

effect 是副作用函数,而 computed 相较于 effect 的区别:

  • computed 可能永远不执行
  • computed 可能被缓存
  • computed 只保证 value 正确,不保证副作用执行

因此,我们不能直接把 computed 作为副作用函数使用。

三、watch 的底层实现

3.1 watch 的本质

watch = 手动创建 effect + 自定义 scheduler

Vue3 中所有 watch/watchEffect 最终都会走到:

doWatch(source, cb, options)

3.2 watch 的 effect 是“非惰性”的

const effect = new ReactiveEffect(getter, scheduler)
  • watch 的 effect 默认立刻收集依赖
  • 后续只要依赖变,就进入 scheduler

3.3 watch 的 getter 是怎么生成的?

情况一:watch ref

watch(count, cb)

getter 实际是:

() => count.value

情况二:watch reactive

watch(state, cb)

getter 实际是:

() => traverse(state) // 深度(deep)监听每一个属性,强制触发所有 getter → 收集所有依赖

3.4 watch 的 scheduler(真正执行 cb)

const job = () => {
  const newValue = effect.run()
  cb(newValue, oldValue) // 在下次触发更新时,对回调函数做调度执行
  oldValue = newValue
}

调度时机由 flush 决定:

3.5 watch vs watchEffect 的区别

非常建议直接看源码,这里 AI 由于不了解源码,经常会产生一些误导性的“发言”。

watchEffect 本质是:没有回调函数,只有 source(fn) 的 watch。

watch 的副作用不是 getter,而是 cb

job = () => {
  const newValue = effect.run()
  cb(newValue, oldValue)
}

依赖收集和副作用执行:

watch(
  () => state.a,
  (newVal, oldVal) => { /* 副作用 */ }
)
依赖收集:
effect.run()
 ↓
执行 getter:() => state.a
 ↓
track(state, 'a')

副作用执行:
state.a 改变
 ↓
trigger
 ↓
scheduler
 ↓
job()
 ↓
cb(newVal, oldVal)
posted @ 2026-01-19 22:28  秀秀不只会前端  阅读(0)  评论(0)    收藏  举报