Open febugcoder opened 1 year ago
看了下, reactive 是能复现的 https://codesandbox.io/s/old-violet-hxcn61?file=/src/index.js
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
我还在想,可能boundary判断需要更精细化一些,现在的问题就是响应来源有多个的时候被过滤掉了,如果做一个响应来源控制,应该是可以解决这个问题的
boundary 判断需要更精细化一些,通过响应源控制是可以控制。这个方法是可行的,我来处理这个 bug
@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 同样代码的效果:
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)
}
})
我发现了当 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
Reproduction link
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
操作步骤
问题
为什么D会计算不出来结果?当C被计算出来之后,为什么D的onFieldReact没有再跑一次?
What is expected?
按操作步骤,能计算出D值
What is actually happening?
按操作步骤,无法计算出D值
Package
@formily/react@2.2.24