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

[Bug Report] formily react被动联动计算与预期不符合 #3837

Open
1 task
febugcoder opened this issue May 30, 2023 · 7 comments · May be fixed by #3892
Open
1 task

[Bug Report] formily react被动联动计算与预期不符合 #3837

febugcoder opened this issue May 30, 2023 · 7 comments · May be fixed by #3892
Labels
bug Something isn't working

Comments

@febugcoder
Copy link
Contributor

  • I have searched the issues of this repository and believe that this is not a duplicate.

Reproduction link

Edit on CodeSandbox

Steps to reproduce

需求

有A、B、C、D四个字段,其中C = A / B,D = C * B

react版本有问题:https://codesandbox.io/s/admiring-glade-puqoq5?file=/src/App.js:539-551

reactive版本没问题:https://codesandbox.io/s/empty-star-pk8sic?file=/src/index.js

操作步骤

  1. 输入A = 2
  2. 输入B = 1
  3. 此时C会计算出来2,但是D没有算出值

问题

为什么D会计算不出来结果?当C被计算出来之后,为什么D的onFieldReact没有再跑一次?

What is expected?

按操作步骤,能计算出D值

What is actually happening?

按操作步骤,无法计算出D值

Package

@formily/react@2.2.24


@janryWang
Copy link
Collaborator

看了下, reactive 是能复现的 https://codesandbox.io/s/old-violet-hxcn61?file=/src/index.js

@Landon-CN
Copy link

Landon-CN commented Jun 8, 2023

setTimeout(() => {
  console.log('change a')
  obs.A = 1
  setTimeout(() => {
    console.log('chagne b')
    obs.B = 2
  }, 1000)
}, 1000)

autorun(() => {
  const A = obs.A
  const B = obs.B
  if (A !== undefined && B !== undefined) {
    obs.C = A / B
    console.log('calc C', obs.C)
  }
}, 'C')

autorun(() => {
  const C = obs.C
  const B = obs.B
  if (C !== undefined && B !== undefined) {
    obs.D = C * B
    console.log('calc D', obs.D)
  }
}, 'D')

以你的demo来看,问题出在 obs.B=2这一步
obs.B=2,这时候 PendingReactions 的值是[autorun D,autorun C]
执行栈如下
-> 执行 autorun D(第一次)
---> autorun D(第一次) finally batchEnd
---> batchEnd继续执行autorun C
-------> autorun C 触发 obs.C = A / B,此时PendingReactions = [autorun D(第二次)]
-------> autorun C finally batchEnd触发 autorun D(第二次)
-------------> 此时 autorun D._boundary = 1 , 走入分支 if (reaction._boundary > 0) return 就是这里导致了计算结果异常
-------> autorun C finally batchEnd 执行结束
---> autorun D(第一次) finally batchEnd 执行结束 reaction._boundary=0

感觉解决方案就是在batchStart执行的途中,假如tracker产生了新的reaction 是否可以使用 microTask的形式异步插入PendingReactions.保证前面的任务执行完毕再执行下一批。
不知道可行不,还没验证
@janryWang

@janryWang
Copy link
Collaborator

我还在想,可能boundary判断需要更精细化一些,现在的问题就是响应来源有多个的时候被过滤掉了,如果做一个响应来源控制,应该是可以解决这个问题的

@hchlq
Copy link
Contributor

hchlq commented Jul 5, 2023

boundary 判断需要更精细化一些,通过响应源控制是可以控制。这个方法是可行的,我来处理这个 bug

@janryWang janryWang added the bug Something isn't working label Oct 18, 2023
@MeetzhDing
Copy link
Contributor

MeetzhDing commented Jan 21, 2024

@janryWang @hchlq 想了解一些,这个boundary主要的意义是什么?我看是在21年引入的,原问题的复现链接已经失效了,没有看懂。

看起来主要目的是为了即让 Reaction 能够循环触发,又不想让它会重复执行最终爆栈。
但 reaction 这个响应式 api 也没有添加这个 boundary 的逻辑?这导致 reaction api 也可能会触发爆栈

在 一个响应式系统中,任务似乎不应当允许自身直接或者间接触发自身吧?

我测试了一下最新的 mobx autorun 逻辑,看起来并没有这个bug

https://runkit.com/embed/zfkqf2qdmx5y

var mobx = require("mobx")

const autorun = mobx.autorun;
const observable = mobx.observable;

const obs = observable({})

setTimeout(() => {
  console.log('change a')
  obs.A = 1
  setTimeout(() => {
    console.log('chagne b')
    obs.B = 2
  }, 1000)
}, 1000)

autorun(() => {
  const A = obs.A
  const B = obs.B
  if (A !== undefined && B !== undefined) {
    obs.C = A / B
    console.log('calc C', obs.C)
  }
})

autorun(() => {
  const C = obs.C
  const B = obs.B
  if (C !== undefined && B !== undefined) {
    obs.D = C * B
    console.log('calc D', obs.D)
  }
})

mobx 同样代码的效果:

image

@MeetzhDing
Copy link
Contributor

MeetzhDing commented Jan 21, 2024

vue/reactive 也没有复现这个问题

https://runkit.com/embed/zqlwosvqxnqy

var reactivity = require("@vue/reactivity")

const autorun = reactivity.effect;
const observable = reactivity.reactive;

const obs = observable({})

setTimeout(() => {
  console.log('change a')
  obs.A = 1
  setTimeout(() => {
    console.log('chagne b')
    obs.B = 2
  }, 1000)
}, 1000)

autorun(() => {
  const A = obs.A
  const B = obs.B
  if (A !== undefined && B !== undefined) {
    obs.C = A / B
    console.log('calc C', obs.C)
  }
})

autorun(() => {
  const C = obs.C
  const B = obs.B
  if (C !== undefined && B !== undefined) {
    obs.D = C * B
    console.log('calc D', obs.D)
  }
})
image

@MeetzhDing
Copy link
Contributor

我发现了当 obs 有初始值 {A:1} 时,这个地方单测就能够通过。
autorun 在每次联动完成以后,如果来源的值是相等的,是否应该是幂等的?

test('autorun with multiple source', async () => {
  // 如果 obs 默认是 {}, 单测会失败
  // const obs = observable<any>({})

  // 如果 obs 默认是 { A: 1 },则单测会通过
  const obs = observable<any>({ A: 1 })

  autorun(() => {
    const A = obs.A
    const B = obs.B
    if (A !== undefined && B !== undefined) {
      obs.C = A / B
      console.log('calc C', obs.C)
    }
  })

  autorun(() => {
    const C = obs.C
    const B = obs.B
    if (C !== undefined && B !== undefined) {
      obs.D = C * B
      console.log('calc D', obs.D)
    }
  })

  setTimeout(() => {
    obs.A = 1
    setTimeout(() => {
      obs.B = 2
    }, 1000)
  }, 500)

  await sleep(3000)
  expect(obs.C).toBe(0.5)
  expect(obs.D).toBe(1)
})

我理解初始值 {A: 1} 在初始化时,是和随后进行 obs.A = 1 的调用,效果应该是等价的?
初始值为 {A: 1} 时,1秒延迟之后,设置 obs.A = 1,发现值相等,什么都不执行。
再过1秒延迟以后,开始执行 obs.B = 2 的联动逻辑。

这里为什么会效果不一致?
这个问题能够解释一下,或者给出可能的使用规避手段吗?
@janryWang @Landon-CN @hchlq @gwsbhqt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants