2025.09.14

防重入锁:阻止无限循环依赖的机制


💥 问题溯源:为什么会产生无限循环?

在响应式系统中,一个经典的性能陷阱是:一个 effect 在单次执行期间同时完成了对同一个响应式数据的“读取”和“写入”操作,这种操作模式会立即触发一个死循环:

  1. 读取: effect 读取 ref.value, 触发 Track。 当前的 effecgt 被登记到了 ref 的订阅者链表中
  2. 写入: effect 立即修改 ref.value, 派发通知(Trigger/Porpagate)
  3. 循环: ref 的订阅链表中有这个 effect, 便重新运行
  4. 读取: effect 重新运行, 从 1 开始重复, 导致 栈溢出

🚨 一段会导致系统崩溃的代码


<script type="module">
  import {ref, effect} from '../dist/reactivity.esm.js'

  const count = ref(0)
  effect(() => {
    // ❗ count.value++ 隐含了:读取 count.value,然后写入 count.value + 1
    console.log(count.value++)
  })
</script>

❗ 根本原因: 在派发通知时,系统没有判断目标 effect 是否正在运行中。

🛡️ 解决方案设计

解决无限循环的核心思路是引入一个防重入(Re-entry Guard) 机制,即在 effect 运行时为其加上一把“执行锁”。

  1. tracking 标记:reactiveEffect 类中增加一个标识(tracking), 来识别 effect 是否在处于 正在运行 的状态
  2. 加锁与解锁:
    • effect 执行前(startTrack), 开锁(tracking = true)
    • effect 执行后(endTrack), 关锁(tracking = false)
  3. 派发通知时的检查: 在派发通知(propagate) 阶段, 只有未加锁(!sub.tracking) 的 effect 才被加入到调度队列

核心代码

  1. ReactiveEffect
    // effect.ts
    export class ReactiveEffect {
    // ... (其他属性)
      tracking = false // 🚀 加锁标志: true 表示 effect 正在运行中
    }
    
  2. effect 周期内管理锁的状态(startTrack endTrack)
    // system.ts
    export function startTrack(sub: ReactiveEffect) {
      sub.tracking = true // 开锁
      sub.depsTail = undefined
    }
    
    export function endTrack(sub: ReactiveEffect) {
      // ... (依赖清理逻辑)
      sub.tracking = false // 解锁
    }
    
  3. 派发通知 propagate
// system.ts
export function propagate(subs: Link) {
  let linkNode = subs
  const queuedEffect = [] // 用于收集待调度的 effect

  while (linkNode) {
    const sub = linkNode.sub

    // 🛡️ 核心检查:只有不在执行中的 effect 才会被调度
    if (!sub.tracking) {
      queuedEffect.push(sub)
    }
    linkNode = linkNode.nextSub
  }

  // 统一执行调度
  queuedEffect.forEach(effect => effect.notify())
}