Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

effect中自增自减死循环 #4

Open
masterX89 opened this issue May 17, 2022 · 0 comments
Open

effect中自增自减死循环 #4

masterX89 opened this issue May 17, 2022 · 0 comments
Labels
reactivity Problems related to reactivity

Comments

@masterX89
Copy link
Owner

问题

响应式中考虑如下的 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 masterX89 added the reactivity Problems related to reactivity label May 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
reactivity Problems related to reactivity
Projects
None yet
Development

No branches or pull requests

1 participant