Skip to content

Commit 76a70ac

Browse files
committed
fix: significantly improve performance by optimizing reactions recollect deps
1 parent ecb5789 commit 76a70ac

File tree

9 files changed

+139
-38
lines changed

9 files changed

+139
-38
lines changed

packages/core/src/models/Field.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export class Field<
160160
componentProps: observable,
161161
validator: observable.shallow,
162162
data: observable.shallow,
163+
parent: observable.computed,
163164
component: observable.computed,
164165
decorator: observable.computed,
165166
errors: observable.computed,

packages/core/src/models/VoidField.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export class VoidField<
8080
data: observable.shallow,
8181
decoratorProps: observable,
8282
componentProps: observable,
83+
parent: observable.computed,
8384
display: observable.computed,
8485
pattern: observable.computed,
8586
hidden: observable.computed,

packages/core/src/shared/internals.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export const patchFieldStates = (
152152
target: Record<string, GeneralField>,
153153
patches: INodePatch<GeneralField>[]
154154
) => {
155-
patches.forEach(({ type, address, oldAddress, payload }) => {
155+
patches.forEach(({ type, address, oldAddress, payload, oldPayload }) => {
156156
if (type === 'remove') {
157157
if (payload) {
158158
// When a payload is passed, the node should be deleted. However, the address may still be used.
@@ -173,7 +173,12 @@ export const patchFieldStates = (
173173
}
174174
}
175175
if (address && payload) {
176-
locateNode(payload, address)
176+
if (oldPayload) {
177+
payload.address = oldPayload.address
178+
payload.path = oldPayload.path
179+
} else {
180+
locateNode(payload, address)
181+
}
177182
}
178183
}
179184
})
@@ -440,6 +445,12 @@ export const spliceArrayState = (
440445
address: newIdentifier,
441446
oldAddress: identifier,
442447
payload: field,
448+
oldPayload: fields[newIdentifier]
449+
? {
450+
address: fields[newIdentifier].address,
451+
path: fields[newIdentifier].path,
452+
}
453+
: undefined,
443454
})
444455
if (isNeedCleanupNode(identifier)) {
445456
fieldPatches.push({

packages/core/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export interface INodePatch<T> {
111111
address: string
112112
oldAddress?: string
113113
payload?: T
114+
oldPayload?: Partial<T>
114115
}
115116

116117
export interface IHeartProps<Context> {

packages/reactive/src/annotations/computed.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { ObModelSymbol, ReactionStack } from '../environment'
1+
import {
2+
ObModelSymbol,
3+
PendingComputedReactions,
4+
PendingScopeComputedReactions,
5+
ReactionStack,
6+
} from '../environment'
27
import { createAnnotation } from '../internals'
38
import { buildDataTree } from '../tree'
49
import { isFn } from '../checkers'
@@ -13,6 +18,7 @@ import {
1318
releaseBindingReactions,
1419
getReactionsFromTargetKey,
1520
} from '../reaction'
21+
import { Reaction } from '../types'
1622

1723
interface IValue<T = any> {
1824
value?: T
@@ -68,6 +74,13 @@ function getPrototypeDescriptor(
6874
return {}
6975
}
7076

77+
const isAllComputedDeps = (reaction: Reaction) => {
78+
if (!reaction._isComputed) return false
79+
const deps = getReactionsFromTargetKey(reaction._context, reaction._property)
80+
if (!deps.length) return true
81+
return deps.every((dep) => isAllComputedDeps(dep))
82+
}
83+
7184
export const computed: IComputed = createAnnotation(
7285
({ target, key, value }) => {
7386
const store: IValue = {}
@@ -95,9 +108,6 @@ export const computed: IComputed = createAnnotation(
95108

96109
reaction._name = 'ComputedReaction'
97110
reaction._scheduler = () => {
98-
if (!reaction._dirty_scheduler) return
99-
reaction._dirty_scheduler = false
100-
101111
if (!reaction._dirty) {
102112
runReactionsFromTargetKey({
103113
target: context,
@@ -112,7 +122,7 @@ export const computed: IComputed = createAnnotation(
112122

113123
if (!deps.length) return
114124

115-
if (deps.every((dep) => dep._isComputed)) {
125+
if (deps.every((dep) => isAllComputedDeps(dep))) {
116126
// all deps are computed reactions, so should dirty the upstream computed reactions
117127
runReactionsFromTargetKey({
118128
target: context,
@@ -140,8 +150,6 @@ export const computed: IComputed = createAnnotation(
140150
reaction._isComputed = true
141151
// is need to re calculate
142152
reaction._dirty = true
143-
// is need to schedule to notice upstream reactions
144-
reaction._dirty_scheduler = true
145153
reaction._context = context
146154
reaction._property = property
147155

@@ -157,9 +165,10 @@ export const computed: IComputed = createAnnotation(
157165
reaction()
158166
reaction._dirty = false
159167
const newValue = store.value
160-
if (newValue === currentValue) {
161-
// no need to schedule
162-
reaction._dirty_scheduler = false
168+
if (newValue !== currentValue) {
169+
// if the value is changed, it should be scheduled
170+
PendingComputedReactions.update(reaction)
171+
PendingScopeComputedReactions.update(reaction)
163172
}
164173
}
165174
bindTargetKeyWithCurrentReaction({

packages/reactive/src/environment.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ObservableListener, Reaction, ReactionsMap } from './types'
22
import { ArraySet } from './array'
3+
import { ReactionsArraySet } from './reactions-array-set'
34
import { DataNode } from './tree'
45

56
export const ProxyRaw = new WeakMap()
@@ -13,10 +14,10 @@ export const BatchCount = { value: 0 }
1314
export const UntrackCount = { value: 0 }
1415
export const BatchScope = { value: false }
1516
export const DependencyCollected = { value: false }
16-
export const PendingReactions = new ArraySet<Reaction>()
17-
export const PendingScopeReactions = new ArraySet<Reaction>()
18-
export const PendingComputedReactions = new ArraySet<Reaction>()
19-
export const PendingScopeComputedReactions = new ArraySet<Reaction>()
17+
export const PendingReactions = new ReactionsArraySet<Reaction>()
18+
export const PendingScopeReactions = new ReactionsArraySet<Reaction>()
19+
export const PendingComputedReactions = new ReactionsArraySet<Reaction>()
20+
export const PendingScopeComputedReactions = new ReactionsArraySet<Reaction>()
2021
export const BatchEndpoints = new ArraySet<() => void>()
2122
export const ObserverListeners = new ArraySet<ObservableListener>()
2223
export const MakeObModelSymbol = Symbol('MakeObModelSymbol')

packages/reactive/src/reaction.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isFn } from './checkers'
22
import { ArraySet } from './array'
3+
import { ReactionsArraySet } from './reactions-array-set'
34
import { IOperation, ReactionsMap, Reaction, PropertyKey } from './types'
45
import {
56
ReactionStack,
@@ -28,28 +29,29 @@ const addRawReactionsMap = (
2829
const reactions = reactionsMap.get(key)
2930
if (reactions) {
3031
reactions.add(reaction)
32+
return reactions
3133
} else {
32-
reactionsMap.set(key, new ArraySet([reaction]))
34+
reactionsMap.set(key, new ReactionsArraySet([reaction]))
35+
return reactionsMap.get(key)
3336
}
34-
return reactionsMap
3537
} else {
3638
const reactionsMap: ReactionsMap = new Map([
37-
[key, new ArraySet([reaction])],
39+
[key, new ReactionsArraySet([reaction])],
3840
])
3941
RawReactionsMap.set(target, reactionsMap)
40-
return reactionsMap
42+
return reactionsMap.get(key)
4143
}
4244
}
4345

44-
const addReactionsMapToReaction = (
46+
const addReactionsSetToReaction = (
4547
reaction: Reaction,
46-
reactionsMap: ReactionsMap
48+
reactionsSet: ReactionsArraySet<Reaction>
4749
) => {
4850
const bindSet = reaction._reactionsSet
4951
if (bindSet) {
50-
bindSet.add(reactionsMap)
52+
bindSet.add(reactionsSet)
5153
} else {
52-
reaction._reactionsSet = new ArraySet([reactionsMap])
54+
reaction._reactionsSet = new ArraySet([reactionsSet])
5355
}
5456
return bindSet
5557
}
@@ -61,9 +63,7 @@ export const getReactionsFromTargetKey = (target: any, key: PropertyKey) => {
6163
const map = reactionsMap.get(key)
6264
if (map) {
6365
map.forEach((reaction) => {
64-
if (reactions.indexOf(reaction) === -1) {
65-
reactions.push(reaction)
66-
}
66+
reactions.push(reaction)
6767
})
6868
}
6969
}
@@ -78,7 +78,6 @@ const runReactions = (target: any, key: PropertyKey) => {
7878
const reaction = reactions[i]
7979
if (reaction._isComputed) {
8080
reaction._dirty = true
81-
reaction._dirty_scheduler = true
8281
if (isScopeBatching()) {
8382
PendingScopeComputedReactions.add(reaction)
8483
} else if (isBatching()) {
@@ -117,7 +116,7 @@ export const bindTargetKeyWithCurrentReaction = (operation: IOperation) => {
117116
if (isUntracking()) return
118117
if (current) {
119118
DependencyCollected.value = true
120-
addReactionsMapToReaction(current, addRawReactionsMap(target, key, current))
119+
addReactionsSetToReaction(current, addRawReactionsMap(target, key, current))
121120
}
122121
}
123122

@@ -158,13 +157,11 @@ export const hasRunningReaction = () => {
158157
}
159158

160159
export const releaseBindingReactions = (reaction: Reaction) => {
161-
reaction._reactionsSet?.forEach((reactionsMap) => {
162-
reactionsMap.forEach((reactions) => {
163-
reactions.delete(reaction)
164-
})
165-
})
166-
PendingReactions.delete(reaction)
167-
PendingScopeReactions.delete(reaction)
160+
// delete outdated reaction by _reactionId
161+
if (typeof reaction._reactionId !== 'number') {
162+
reaction._reactionId = 0
163+
} else reaction._reactionId++
164+
168165
delete reaction._reactionsSet
169166
}
170167

@@ -183,6 +180,11 @@ export const suspendComputedReactions = (current: Reaction) => {
183180

184181
export const disposeBindingReactions = (reaction: Reaction) => {
185182
reaction._disposed = true
183+
184+
reaction._reactionsSet?.forEach((reactionsSet) => {
185+
reactionsSet.delete(reaction)
186+
})
187+
186188
releaseBindingReactions(reaction)
187189
suspendComputedReactions(reaction)
188190
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Reaction } from './types'
2+
3+
/**
4+
* work like arrayset but delete outdated reaction by _reactionId
5+
*/
6+
export class ReactionsArraySet<T extends Reaction> {
7+
valueSet: Set<T>
8+
reactionIdMap: Map<T, number>
9+
forEachIndex = 0
10+
constructor(value: T[] = []) {
11+
this.valueSet = new Set()
12+
this.reactionIdMap = new Map()
13+
value.forEach((item) => {
14+
this.add(item)
15+
})
16+
}
17+
18+
add(item: T) {
19+
if (!this.has(item)) {
20+
this.valueSet.add(item)
21+
this.reactionIdMap.set(item, item._reactionId || 0)
22+
} else {
23+
this.reactionIdMap.set(item, item._reactionId || 0)
24+
}
25+
}
26+
27+
has(item: T) {
28+
return this.valueSet.has(item)
29+
}
30+
31+
update(item: T) {
32+
if (this.valueSet.has(item)) {
33+
this.reactionIdMap.set(item, item._reactionId || 0)
34+
}
35+
}
36+
37+
delete(item: T) {
38+
const size = this.valueSet.size
39+
if (size === 0) return
40+
this.valueSet.delete(item)
41+
this.reactionIdMap.delete(item)
42+
}
43+
44+
forEach(callback: (value: T) => void) {
45+
if (this.valueSet.size === 0) return
46+
for (const item of this.valueSet) {
47+
const reactionId = this.reactionIdMap.get(item)
48+
if (reactionId === item._reactionId) {
49+
callback(item)
50+
} else {
51+
this.delete(item)
52+
}
53+
}
54+
}
55+
56+
batchDelete(callback: (value: T) => void) {
57+
if (this.valueSet.size === 0) return
58+
59+
for (const item of this.valueSet) {
60+
const reactionId = this.reactionIdMap.get(item)
61+
if (reactionId === item._reactionId) {
62+
callback(item)
63+
}
64+
}
65+
66+
this.clear()
67+
}
68+
69+
clear() {
70+
this.valueSet.clear()
71+
this.reactionIdMap.clear()
72+
}
73+
}

packages/reactive/src/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ArraySet } from './array'
2+
import { ReactionsArraySet } from './reactions-array-set'
23

34
export * from './tree'
45

@@ -69,7 +70,8 @@ export type Reaction = ((...args: any[]) => any) & {
6970
_disposed?: boolean
7071
_property?: PropertyKey
7172
_computesSet?: ArraySet<Reaction>
72-
_reactionsSet?: ArraySet<ReactionsMap>
73+
_reactionsSet?: ArraySet<ReactionsArraySet<Reaction>>
74+
_reactionId?: number
7375
_scheduler?: (reaction: Reaction) => void
7476
_memos?: {
7577
queue: IMemoQueueItem[]
@@ -81,7 +83,7 @@ export type Reaction = ((...args: any[]) => any) & {
8183
}
8284
}
8385

84-
export type ReactionsMap = Map<PropertyKey, ArraySet<Reaction>>
86+
export type ReactionsMap = Map<PropertyKey, ReactionsArraySet<Reaction>>
8587

8688
export interface IReactionOptions<T> {
8789
name?: string

0 commit comments

Comments
 (0)