Skip to content

D06 · effect / track / trigger 三件套 ​

对应主课: L31-L32 响应式原理 + 依赖追踪 最后核对: 2026-09-23


1. 三者的关系 ​


2. 数据结构 ​

下面是教学用的 Set 模型,便于观察订阅关系;Vue 3.5.43 使用 Dep、Subscriber、带版本号的双向 Link 管理连接,并不逐字等于这个结构。

targetMap (WeakMap)
  └─ target 对象 → depsMap (Map)
       ├─ 'count'  → deps (Set) → { effectA, effectB }
       └─ 'name'   → deps (Set) → { effectC }
  • WeakMap:映射本身不让 target 因为充当键而一直存活;其他闭包或对象仍可能持有 target,GC 也不能代替 stop 与外部资源清理
  • Map:每个属性一个依赖集合
  • Set:同一个 effect 不会重复收集

3. track 实现 ​

完整代码在 §7。track 的关键是同时去重两边的连接:

javascript
// dep 是当前 target/key 的 Set;activeEffect 是正在收集依赖的实例。
if (activeEffect?.active && !dep.has(activeEffect)) {
  dep.add(activeEffect)
  activeEffect.deps.push(dep)
}

只在 Set 里去重还不够:若每次读同一属性都 push,effect.deps 会重复保存同一个集合。反向列表用于 cleanup,在重跑或 stop 时把 effect 从旧集合移走。


4. trigger 实现 ​

触发时遍历依赖快照,避免 effect 在 cleanup 后又重新加入正在遍历的 Set。只处理仍活动、且当前不在执行栈中的 effect;有 scheduler 则传入同一个稳定 runner,由调度器决定何时执行:

javascript
for (const item of new Set(dep)) {
  if (!item.active || item.running) continue
  if (item.scheduler) item.scheduler(item.runner)
  else item.run()
}

这段是 §7 的局部逻辑。§7 默认同步执行,不把 effect 等同于组件更新的异步 job;接入 L35 的队列才会批处理。

为什么跳过正在执行的 effect ​

如果 effect 内既读又写同一个 count,写入可能立即再调用自己。running guard 可以阻止当前调用栈里的自重入,也能保护仍在运行的外层 effect。不过它不保证消除多个 effect 或异步回调形成的所有反馈循环。业务上仍应避免在派生计算或渲染中无条件修改自己的来源。


5. effect 的 cleanup ​

这是“全清后重收”的教学算法。真实 Vue 3.5.43 会在运行前准备依赖版本,再清理本次未使用的 Link;run 前后还要恢复活动订阅者,不能让嵌套 effect 或异常破坏外层上下文。固定版本 effect.ts


6. 在 Vue 组件中的应用 ​

场景内部机制
模板绑定render effect 中读取 → track
computed(() => ...)3.5 的 ComputedRefImpl 自身是 Subscriber,按依赖/全局版本惰性刷新;L32 的 effect+dirty 是教学模型
watch(source, cb)effect 读取来源;job 检查变化与清理后调用 cb,受 flush 等选项影响
watchEffect(() => ...)在同步执行段追踪依赖;首次与后续运行时序受 flush 影响

7. 动手实验:可清理、可停止的教学模型 ​

在现代浏览器控制台一次性运行。这个模型只处理普通对象的浅层数据属性,不实现嵌套转换、枚举、数组/集合、computed 或组件。L32 继续展示嵌套响应式、computed 与 ref 的组合:

javascript
function createMiniReactivity() {
  const targetMap = new WeakMap()
  const cache = new WeakMap()
  const proxies = new WeakSet()
  let activeEffect
  function cleanup(item) {
    for (const dep of item.deps) dep.delete(item)
    item.deps.length = 0
  }
  class ReactiveEffect {
    deps = []
    active = true
    running = false
    constructor(fn, scheduler) {
      this.fn = fn
      this.scheduler = scheduler
      this.runner = this.run.bind(this)
      this.runner.effect = this
    }
    run() {
      if (!this.active) return this.fn()
      if (this.running) return
      cleanup(this)
      const previous = activeEffect
      activeEffect = this
      this.running = true
      try { return this.fn() }
      finally { activeEffect = previous; this.running = false }
    }
    stop() {
      this.active = false
      cleanup(this)
    }
  }
  function track(target, key) {
    if (!activeEffect?.active) return
    let depsMap = targetMap.get(target)
    if (!depsMap) targetMap.set(target, depsMap = new Map())
    let dep = depsMap.get(key)
    if (!dep) depsMap.set(key, dep = new Set())
    if (!dep.has(activeEffect)) {
      dep.add(activeEffect)
      activeEffect.deps.push(dep)
    }
  }
  function trigger(target, key) {
    const dep = targetMap.get(target)?.get(key)
    if (!dep) return
    for (const item of new Set(dep)) {
      if (!item.active || item.running) continue
      if (item.scheduler) item.scheduler(item.runner)
      else item.run()
    }
  }
  function reactive(target) {
    if (target === null || typeof target !== 'object') throw new Error('需要普通对象')
    const proto = Object.getPrototypeOf(target)
    if (proto !== Object.prototype && proto !== null) throw new Error('只支持普通对象')
    if (proxies.has(target)) return target
    if (cache.has(target)) return cache.get(target)
    const proxy = new Proxy(target, {
      get(object, key, receiver) {
        const value = Reflect.get(object, key, receiver)
        track(object, key)
        return value
      },
      set(object, key, value, receiver) {
        const had = Object.prototype.hasOwnProperty.call(object, key)
        const old = Reflect.get(object, key, receiver)
        const result = Reflect.set(object, key, value, receiver)
        if (result && (!had || !Object.is(old, value))) trigger(object, key)
        return result
      },
      deleteProperty(object, key) {
        const had = Object.prototype.hasOwnProperty.call(object, key)
        const result = Reflect.deleteProperty(object, key)
        if (result && had) trigger(object, key)
        return result
      },
    })
    cache.set(target, proxy)
    proxies.add(proxy)
    return proxy
  }
  function effect(fn, { scheduler, lazy = false } = {}) {
    const item = new ReactiveEffect(fn, scheduler)
    if (!lazy) {
      try { item.run() } catch (error) { item.stop(); throw error }
    }
    return item.runner
  }
  function stop(runner) { runner.effect.stop() }
  return { reactive, effect, stop }
}

const { reactive, effect, stop } = createMiniReactivity()
const state = reactive({ showA: true, a: 1, b: 10, count: 0 })
let runs = 0
let selected
const branch = effect(() => {
  runs++
  selected = state.showA ? state.a : state.b
  console.log('选中值:', selected)
})
state.a = 2
state.showA = false
state.a = 3 // 已不再订阅 a
console.assert(runs === 3 && selected === 10)
state.b = 11
stop(branch)
state.b = 12
console.assert(runs === 4 && selected === 11)
branch() // 已停止 runner 可以手动执行,但不重新成为订阅者
state.b = 13
console.assert(runs === 5 && selected === 12)

const jobs = new Set()
let seen = -1
const queued = effect(() => { seen = state.count }, { scheduler: job => jobs.add(job) })
state.count = 1
state.count = 2
console.assert(jobs.size === 1 && seen === 0)
for (const job of jobs) if (job.effect.active) job()
jobs.clear()
console.assert(seen === 2)
stop(queued)

这里的 scheduler 参数是稳定 runner,而不是每次触发新建的函数,也不是 ReactiveEffect 实例本身。队列调用前检查 active,避免把已经停止的 runner 当成一次手动执行。run 的 try/finally 保证异常后恢复上下文;当前同步模型会把异常交给调用者,不实现 Vue 的组件错误处理。

ref 并不是 reactive({ value: T }) 的语法简写:Vue 的 RefImpl 用 .value getter/setter,普通对象值才进一步转成响应式代理。3.5 computed 的实现也不同于此 mini;对应源码见 ref.ts、computed.ts。


8. 总结 ​

  • 本篇 effect 默认立即执行并返回 runner;lazy 是教学模型的选项,不应据此推断所有 Vue 高层 API 的首次时序
  • track 在 getter 中收集"谁在用这个数据"
  • trigger 在 setter 中通知"数据变了,重新执行"
  • cleanup 解决条件分支的依赖残留
  • scheduler 接管执行时机,配合稳定 runner 去重;是否异步由具体调度器决定