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

fix: multi source update #3890

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions packages/reactive/src/__tests__/autorun.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ test('autorun', () => {
expect(handler).toBeCalledTimes(2)
})

test('autorun first argument is not a function', () => {
autorun({} as any)
autorun(1 as any)
autorun('1' as any)
})

test('reaction', () => {
const obs = observable({
aa: {
Expand Down Expand Up @@ -741,3 +747,51 @@ test('reaction recollect dependencies', () => {
expect(fn2).toBeCalledTimes(2)
expect(trigger2).toBeCalledTimes(2)
})

test('multiple source update', () => {
const obs = observable<any>({})

const fn1 = jest.fn()
const fn2 = jest.fn()

autorun(() => {
const A = obs.A
const B = obs.B
if (A !== undefined && B !== undefined) {
obs.C = A / B
fn1()
}
})

autorun(() => {
const C = obs.C
const B = obs.B
if (C !== undefined && B !== undefined) {
obs.D = C * B
fn2()
}
})

obs.A = 1
obs.B = 2

expect(fn1).toBeCalledTimes(1)
expect(fn2).toBeCalledTimes(1)
})

test('same source in nest update', () => {
const obs = observable<any>({})

const fn1 = jest.fn()

autorun(() => {
const B = obs.B
obs.B = 'B'
fn1()
return B
})

obs.B = 'B2'

expect(fn1).toBeCalledTimes(2)
})
56 changes: 55 additions & 1 deletion packages/reactive/src/__tests__/tracker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Tracker, observable } from '../'
import { Tracker, observable, autorun } from '../'

test('base tracker', () => {
const obs = observable<any>({})
Expand All @@ -18,6 +18,13 @@ test('base tracker', () => {
tracker.dispose()
})

test('track argument is not a function', () => {
const scheduler = () => {}
const tracker = new Tracker(scheduler)

tracker.track({})
})

test('nested tracker', () => {
const obs = observable<any>({})
const fn = jest.fn()
Expand Down Expand Up @@ -91,3 +98,50 @@ test('shared scheduler with multi tracker(mock react strict mode)', () => {
expect(scheduler1).toBeCalledTimes(1)
expect(scheduler2).toBeCalledTimes(0)
})

test('multiple source update', () => {
const obs = observable<any>({})

const fn1 = jest.fn()
const fn2 = jest.fn()

const view1 = () => {
const A = obs.A
const B = obs.B
if (A !== undefined && B !== undefined) {
obs.C = A / B
fn1()
}
}
const scheduler1 = () => {
tracker1.track(view1)
}

const tracker1 = new Tracker(scheduler1)

const view2 = () => {
const C = obs.C
const B = obs.B
if (C !== undefined && B !== undefined) {
obs.D = C * B
fn2()
}
}
const scheduler2 = () => {
tracker2.track(view2)
}

const tracker2 = new Tracker(scheduler2)

tracker1.track(view1)
tracker2.track(view2)

obs.A = 1
obs.B = 2

expect(fn1).toBeCalledTimes(1)
expect(fn2).toBeCalledTimes(1)

tracker1.dispose()
tracker2.dispose()
})
15 changes: 11 additions & 4 deletions packages/reactive/src/autorun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ interface IValue {
export const autorun = (tracker: Reaction, name = 'AutoRun') => {
const reaction: Reaction = () => {
if (!isFn(tracker)) return
if (reaction._boundary > 0) return

const updateKey = reaction._boundary.get(reaction._updateTarget)
if (updateKey && updateKey === reaction._updateKey) return

if (ReactionStack.indexOf(reaction) === -1) {
releaseBindingReactions(reaction)
try {
Expand All @@ -28,9 +31,11 @@ export const autorun = (tracker: Reaction, name = 'AutoRun') => {
tracker()
} finally {
ReactionStack.pop()
reaction._boundary++
if (reaction._updateKey) {
reaction._boundary.set(reaction._updateTarget, reaction._updateKey)
}
batchEnd()
reaction._boundary = 0
reaction._boundary.clear()
reaction._memos.cursor = 0
reaction._effects.cursor = 0
}
Expand All @@ -46,10 +51,12 @@ export const autorun = (tracker: Reaction, name = 'AutoRun') => {
cursor: 0,
}
}
reaction._boundary = 0

reaction._boundary = new Map()
reaction._name = name
cleanRefs()
reaction()

return () => {
disposeBindingReactions(reaction)
disposeEffects(reaction)
Expand Down
2 changes: 2 additions & 0 deletions packages/reactive/src/reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const runReactions = (target: any, key: PropertyKey) => {
UntrackCount.value = 0
for (let i = 0, len = reactions.length; i < len; i++) {
const reaction = reactions[i]
reaction._updateTarget = target
reaction._updateKey = key
if (reaction._isComputed) {
reaction._scheduler(reaction)
} else if (isScopeBatching()) {
Expand Down
18 changes: 13 additions & 5 deletions packages/reactive/src/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ export class Tracker {
name = 'TrackerReaction'
) {
this.track._scheduler = (callback) => {
if (this.track._boundary === 0) this.dispose()
if (this.track._boundary.size === 0) this.dispose()
if (isFn(callback)) scheduler(callback)
}
this.track._name = name
this.track._boundary = 0
this.track._boundary = new Map()
}

track: Reaction = (tracker: Reaction) => {
if (!isFn(tracker)) return this.results
if (this.track._boundary > 0) return

const updateKey = this.track._boundary.get(this.track._updateTarget)
if (updateKey && updateKey === this.track._updateKey) return

if (ReactionStack.indexOf(this.track) === -1) {
releaseBindingReactions(this.track)
try {
Expand All @@ -33,9 +36,14 @@ export class Tracker {
this.results = tracker()
} finally {
ReactionStack.pop()
this.track._boundary++
if (this.track._updateKey) {
this.track._boundary.set(
this.track._updateTarget,
this.track._updateKey
)
}
batchEnd()
this.track._boundary = 0
this.track._boundary.clear()
}
}
return this.results
Expand Down
4 changes: 3 additions & 1 deletion packages/reactive/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,14 @@ export type Dispose = () => void
export type Effect = () => void | Dispose

export type Reaction = ((...args: any[]) => any) & {
_boundary?: number
_boundary?: Map<any, any>
_name?: string
_isComputed?: boolean
_dirty?: boolean
_context?: any
_disposed?: boolean
_updateKey?: string
_updateTarget?: any
_property?: PropertyKey
_computesSet?: ArraySet<Reaction>
_reactionsSet?: ArraySet<ReactionsMap>
Expand Down