mini-vue icon indicating copy to clipboard operation
mini-vue copied to clipboard

effect中自增自减死循环

Open masterX89 opened this issue 3 years ago • 0 comments

问题

响应式中考虑如下的 case,为何可能会陷入死循环,以及如何解决?

it('should avoid implicit infinite recursive loops with itself', () => {
  const counter = reactive({ num: 0 })
  effect(() => counter.num++)
  expect(counter.num).toBe(1)
})

分析

counter.num++ 实际可以写成这种展开形式

counter.num = counter.num + 1

意味着这条语句同时有 gettersetter,先 getter 后执行 setter。这时候应该有直觉会陷入死循环里了,所以我们可以开个 debug 看下。会发现执行顺序是:

  • 进入 effect 函数中执行 run()
  • run 函数中调用 this.fn(),即 () => counter.num++
  • getter 中执行 track,返回当前值
  • 加 1 后 setter 中执行 trigger
  • trigger 中拿到 target -> key -> dep -> effect 实例 执行
  • 再次进入 run

从而发生了死循环。

分析结束可以得出结论,在当前的 activeEffect 正在执行的过程中,如果再次执行该 effect,即会陷入死循环。因此可以在 trigger 内部中判断当前的 effect 和 activeEffect 是否相等即可。

解决方案

function triggerEffect(effect: any) {
  if (effect !== activeEffect) {
    // 添加这个判断以避免死循环
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

继续深入

在知道了避免死循环的方案后,我们按照尤大的方法继续把 case 变得更加严格:

it('should avoid implicit infinite recursive loops with itself', () => {
  const counter = reactive({ num: 0 })
  const counterSpy = jest.fn(() => counter.num++)
  effect(counterSpy)
  expect(counter.num).toBe(1)
  expect(counterSpy).toHaveBeenCalledTimes(1)
  counter.num = 4
  expect(counter.num).toBe(5)
  expect(counterSpy).toHaveBeenCalledTimes(2)
})

你会发现按照崔大的写法 case 中的这句过不去了

expect(counter.num).toBe(5)

原因也很简单,我们忘记在 ReactiveEffectrun 方法中,在执行结束后清理现场了。否则 activeEffect 明明已经执行结束了,却还保留有值,导致新的 effect 无法进入。修改为下述代码即可

run() {
  if (!this.active) {
    return this._fn()
  }
  shouldTrack = true
  activeEffect = this
  const result = this._fn()
  shouldTrack = false
  // 除了 shouldTrack 需要改为 false 外
  // activeEffect 也需要清理现场为 undefined
  activeEffect = undefined
  return result
}

总结

响应式自增自减产生死循环的情况在 HCY 的书中和群里都有人提及,因此在这里写篇文章记录一下。

其实 activeEffect 这样写还是存在问题,因为如果只有一个 activeEffect,如果是嵌套的 effect 的话,就会丢失上下文环境,因此最好要写成栈的形式,我后续会继续补充。

masterX89 avatar May 17 '22 14:05 masterX89