Skip to content

Commit 5dd1710

Browse files
committed
fix(reactive): avoid chain reactions missing
1 parent ecb5789 commit 5dd1710

File tree

6 files changed

+240
-18
lines changed

6 files changed

+240
-18
lines changed

packages/reactive/src/__tests__/autorun.spec.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,3 +757,207 @@ test('avoid unnecessary reaction', () => {
757757

758758
expect(fn1).toBeCalledTimes(1)
759759
})
760+
761+
test('avoid missing reaction', () => {
762+
const obs = observable<any>({
763+
res: 0,
764+
})
765+
766+
const fn1 = jest.fn()
767+
const fn2 = jest.fn()
768+
769+
let value
770+
autorun(() => {
771+
fn1()
772+
value = obs.res
773+
})
774+
775+
autorun(() => {
776+
fn2()
777+
obs.res
778+
if (obs.res !== 0) obs.res = obs.res + 1
779+
})
780+
781+
expect(value).toBe(0)
782+
783+
obs.res = 1
784+
785+
expect(value).toBe(2)
786+
787+
expect(fn1).toBeCalledTimes(3)
788+
expect(fn2).toBeCalledTimes(2)
789+
})
790+
791+
test('avoid reaction twice', () => {
792+
const obs = observable<any>({
793+
res: 0,
794+
})
795+
796+
const fn1 = jest.fn()
797+
const fn2 = jest.fn()
798+
799+
let value
800+
autorun(() => {
801+
fn1()
802+
obs.res
803+
if (obs.res !== 0) obs.res = obs.res + 1
804+
})
805+
806+
autorun(() => {
807+
fn2()
808+
value = obs.res
809+
})
810+
811+
expect(value).toBe(0)
812+
813+
obs.res = 1
814+
815+
expect(value).toBe(2)
816+
expect(fn1).toBeCalledTimes(2)
817+
expect(fn2).toBeCalledTimes(2)
818+
})
819+
820+
test('computed reaction', () => {
821+
const obs = observable<any>({
822+
aa: 1,
823+
bb: 1,
824+
})
825+
826+
const computed = observable.computed(() => {
827+
return obs.aa + obs.bb
828+
})
829+
830+
autorun(() => {
831+
if (obs.bb === 3) {
832+
obs.aa = 3
833+
}
834+
})
835+
836+
batch(() => {
837+
obs.aa = 2 // 会触发 computed 发生变化
838+
839+
obs.bb = 3
840+
})
841+
842+
expect(computed.value).toBe(6)
843+
})
844+
845+
test('accurate boundary', () => {
846+
const obs = observable<any>({
847+
a: '',
848+
b: '',
849+
c: '',
850+
})
851+
852+
autorun(() => {
853+
obs.c = obs.a + obs.b
854+
})
855+
856+
autorun(() => {
857+
obs.b = obs.a
858+
})
859+
860+
obs.a = 'a'
861+
expect(obs.a).toBe('a')
862+
expect(obs.b).toBe('a')
863+
expect(obs.c).toBe('aa')
864+
})
865+
866+
test('multiple source update', () => {
867+
const obs = observable<any>({})
868+
869+
const fn1 = jest.fn()
870+
const fn2 = jest.fn()
871+
872+
autorun(() => {
873+
const A = obs.A
874+
const B = obs.B
875+
if (A !== undefined && B !== undefined) {
876+
obs.C = A / B
877+
fn1()
878+
}
879+
})
880+
881+
autorun(() => {
882+
const C = obs.C
883+
const B = obs.B
884+
if (C !== undefined && B !== undefined) {
885+
obs.D = C * B
886+
fn2()
887+
}
888+
})
889+
890+
obs.A = 1
891+
obs.B = 2
892+
893+
expect(fn1).toBeCalledTimes(1)
894+
expect(fn2).toBeCalledTimes(1)
895+
})
896+
897+
test('same source in nest update', () => {
898+
const obs = observable<any>({})
899+
900+
const fn1 = jest.fn()
901+
902+
autorun(() => {
903+
const B = obs.B
904+
obs.B = 'B'
905+
fn1()
906+
return B
907+
})
908+
909+
obs.B = 'B2'
910+
911+
expect(fn1).toBeCalledTimes(2)
912+
})
913+
914+
test('batch execute autorun cause by deep indirect dependency', () => {
915+
const obs: any = observable({ aa: 1, bb: 1, cc: 1 })
916+
const fn = jest.fn()
917+
const fn2 = jest.fn()
918+
const fn3 = jest.fn()
919+
920+
autorun(() => fn((obs.aa = obs.bb + obs.cc)))
921+
autorun(() => fn2((obs.bb = obs.aa + obs.cc)))
922+
autorun(() => fn3((obs.cc = obs.aa + obs.bb)))
923+
924+
// 嵌套写法重复调用没意义,只需要确保最新被触发的 reaction 执行,已过时的 reaction 可以忽略
925+
// 比如 fn3 执行,触发 fn 和 fn2,fn 执行又触发 fn2,之前触发的 fn2 是过时的,忽略处理,fn2 只执行一次
926+
expect(fn).toBeCalledTimes(3)
927+
expect(fn2).toBeCalledTimes(2)
928+
expect(fn3).toBeCalledTimes(1)
929+
930+
fn.mockClear()
931+
fn2.mockClear()
932+
fn3.mockClear()
933+
934+
batch(() => {
935+
obs.aa = 100
936+
obs.bb = 100
937+
obs.cc = 100
938+
})
939+
940+
expect(fn).toBeCalledTimes(1)
941+
expect(fn2).toBeCalledTimes(1)
942+
expect(fn3).toBeCalledTimes(1)
943+
})
944+
945+
test('multiple update should trigger only one', () => {
946+
const obs = observable({ aa: 1, bb: 1 })
947+
948+
autorun(() => {
949+
obs.aa = obs.bb + 1
950+
obs.bb = obs.aa + 1
951+
})
952+
953+
expect(obs.aa).toBe(2)
954+
expect(obs.bb).toBe(3)
955+
956+
autorun(() => {
957+
obs.aa = obs.bb + 1
958+
obs.bb = obs.aa + 1
959+
})
960+
961+
expect(obs.aa).toBe(6)
962+
expect(obs.bb).toBe(7)
963+
})

packages/reactive/src/__tests__/tracker.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,5 @@ test('shared scheduler with multi tracker(mock react strict mode)', () => {
8989
obs.value = 123
9090

9191
expect(scheduler1).toBeCalledTimes(1)
92-
expect(scheduler2).toBeCalledTimes(0)
92+
expect(scheduler2).toBeCalledTimes(1)
9393
})

packages/reactive/src/array.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,15 @@ export class ArraySet<T> {
4949

5050
batchDelete(callback: (value: T) => void) {
5151
if (this.value.length === 0) return
52-
this.forEachIndex = 0
53-
for (; this.forEachIndex < this.value.length; this.forEachIndex++) {
54-
const value = this.value[this.forEachIndex]
55-
this.value.splice(this.forEachIndex, 1)
56-
this.forEachIndex--
57-
callback(value)
52+
53+
const batchList = this.value.splice(0, this.value.length)
54+
55+
for (let i = 0; i < batchList.length; i++) {
56+
callback(batchList[i])
57+
}
58+
59+
if (this.value.length > 0) {
60+
this.batchDelete(callback)
5861
}
5962
}
6063

packages/reactive/src/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const RawReactionsMap = new WeakMap<object, ReactionsMap>()
1111
export const ReactionStack: Reaction[] = []
1212
export const BatchCount = { value: 0 }
1313
export const UntrackCount = { value: 0 }
14+
export const BatchIdRef = { current: 1 }
1415
export const BatchScope = { value: false }
1516
export const DependencyCollected = { value: false }
1617
export const PendingReactions = new ArraySet<Reaction>()

packages/reactive/src/reaction.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ObserverListeners,
1515
PendingComputedReactions,
1616
PendingScopeComputedReactions,
17+
BatchIdRef,
1718
} from './environment'
1819

1920
const ITERATION_KEY = Symbol('iteration key')
@@ -222,13 +223,7 @@ export const batchScopeEnd = () => {
222223
})
223224

224225
BatchScope.value = false
225-
PendingScopeReactions.batchDelete((reaction) => {
226-
if (isFn(reaction._scheduler)) {
227-
reaction._scheduler(reaction)
228-
} else {
229-
reaction()
230-
}
231-
})
226+
executePendingScopeReactions()
232227
UntrackCount.value = prevUntrackCount
233228
}
234229

@@ -255,11 +250,29 @@ export const executePendingComputedReactions = () => {
255250
}
256251

257252
export const executePendingReactions = () => {
253+
const batchId = BatchIdRef.current++
258254
PendingReactions.batchDelete((reaction) => {
259-
if (isFn(reaction._scheduler)) {
260-
reaction._scheduler(reaction)
261-
} else {
262-
reaction()
255+
if (batchId > (reaction._batchId || 0)) {
256+
reaction._batchId = batchId
257+
if (isFn(reaction._scheduler)) {
258+
reaction._scheduler(reaction)
259+
} else {
260+
reaction()
261+
}
262+
}
263+
})
264+
}
265+
266+
export const executePendingScopeReactions = () => {
267+
const batchId = BatchIdRef.current++
268+
PendingScopeReactions.batchDelete((reaction) => {
269+
if (batchId > (reaction._batchId || 0)) {
270+
reaction._batchId = batchId
271+
if (isFn(reaction._scheduler)) {
272+
reaction._scheduler(reaction)
273+
} else {
274+
reaction()
275+
}
263276
}
264277
})
265278
}

packages/reactive/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export type Reaction = ((...args: any[]) => any) & {
7070
_property?: PropertyKey
7171
_computesSet?: ArraySet<Reaction>
7272
_reactionsSet?: ArraySet<ReactionsMap>
73+
_batchId?: number
7374
_scheduler?: (reaction: Reaction) => void
7475
_memos?: {
7576
queue: IMemoQueueItem[]

0 commit comments

Comments
 (0)