Skip to content

effect中自增自减死循环 #4

Open
@masterX89

Description

@masterX89

问题

响应式中考虑如下的 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 的话,就会丢失上下文环境,因此最好要写成栈的形式,我后续会继续补充。

Metadata

Metadata

Assignees

No one assigned

    Labels

    reactivityProblems related to reactivity

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions