Skip to content

Commit 210e7eb

Browse files
committed
improve perf
1 parent 1f876d0 commit 210e7eb

8 files changed

Lines changed: 140 additions & 125 deletions

File tree

src/api/extras.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {IDepTreeNode} from "../core/observable";
1+
import {IDepTreeNode, isObjectObservable, getObservers, hasObservers} from "../core/observable";
22
import {unique} from "../utils/utils";
33
import {getAtom} from "../types/type-utils";
44

@@ -33,7 +33,7 @@ function nodeToObserverTree(node: IDepTreeNode): IObserverTree {
3333
const result: IObserverTree = {
3434
name: node.name
3535
};
36-
if (node.observers && node.observers.length > 0)
37-
result.observers = <any>node.observers.asArray().map(<any>nodeToObserverTree);
36+
if (isObjectObservable(node) && hasObservers(node))
37+
result.observers = <any>getObservers(node).map(<any>nodeToObserverTree);
3838
return result;
3939
}

src/core/atom.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export interface IAtom extends IObservable {
1111
export class BaseAtom implements IAtom {
1212
isPendingUnobservation: boolean; // for effective unobserving
1313
isObserved = false;
14-
observers = new DerivationsSets();
14+
_observers = [];
15+
_observersToDelete = [];
1516

1617
diffValue = 0;
1718
lastAccessedBy = 0;
@@ -26,6 +27,9 @@ export class BaseAtom implements IAtom {
2627
// noop
2728
}
2829

30+
get observers() {
31+
return legacyObservers(this);
32+
}
2933
/**
3034
* Invoke this method to notify mobx that your atom has been used somehow.
3135
*/
@@ -75,6 +79,6 @@ export class Atom extends BaseAtom implements IAtom {
7579
}
7680

7781
import {globalState} from "./globalstate";
78-
import {IObservable, propagateChanged, reportObserved, DerivationsSets} from "./observable";
82+
import {IObservable, propagateChanged, reportObserved, legacyObservers} from "./observable";
7983
import {transactionStart, transactionEnd} from "../core/transaction";
8084
import {noop, getNextId} from "../utils/utils";

src/core/computedvalue.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {IObservable, reportObserved, propagateMaybeChanged, propagateChangeConfirmed, DerivationsSets, startBatch, endBatch} from "./observable";
1+
import {IObservable, reportObserved, propagateMaybeChanged, propagateChangeConfirmed, legacyObservers, startBatch, endBatch, getObservers} from "./observable";
22
import {IDerivation, trackDerivedFunction, clearObserving, untrackedStart, untrackedEnd, shouldCompute } from "./derivation";
33
import {globalState} from "./globalstate";
44
import {allowStateChangesStart, allowStateChangesEnd} from "./action";
@@ -27,7 +27,8 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
2727

2828
isPendingUnobservation: boolean; // for effective unobserving
2929
isObserved = false;
30-
observers = new DerivationsSets();
30+
_observers = [];
31+
_observersToDelete = [];
3132

3233
diffValue = 0;
3334
runId = 0;
@@ -51,6 +52,10 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
5152
this.name = name || "ComputedValue@" + getNextId();
5253
}
5354

55+
get observers() {
56+
return legacyObservers(this);
57+
}
58+
5459
peek() {
5560
this.isComputing = true;
5661
const prevAllowStateChanges = allowStateChangesStart(false);
@@ -139,7 +144,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
139144
whyRun() {
140145
const isTracking = globalState.derivationStack.length > 0;
141146
const observing = unique(this.observing).map(dep => dep.name);
142-
const observers = unique(this.observers.asArray().map(dep => dep.name));
147+
const observers = unique(getObservers(this).map(dep => dep.name));
143148
const runReason = (
144149
this.isComputing
145150
? isTracking

src/core/globalstate.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {IDerivation} from "./derivation";
22
import {Reaction} from "./reaction";
3-
import {IObservable, DerivationsSets} from "./observable";
3+
import {IObservable} from "./observable";
44

55
declare const global: any;
66

@@ -15,7 +15,7 @@ export class MobXGlobals {
1515
* MobX compatiblity with other versions loaded in memory as long as this version matches.
1616
* It indicates that the global state still stores similar information
1717
*/
18-
version = 3;
18+
version = 4;
1919

2020
/**
2121
* Stack of currently running derivations
@@ -52,7 +52,7 @@ export class MobXGlobals {
5252

5353
pendingUnobservations: IObservable[] = [];
5454

55-
pendingDeletions: DerivationsSets[] = [];
55+
pendingDeletions: IObservable[] = [];
5656
/**
5757
* List of scheduled, not yet executed, reactions.
5858
*/

src/core/observable.ts

Lines changed: 76 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,14 @@ import {IDerivation} from "./derivation";
22
import {globalState} from "./globalstate";
33
import {invariant} from "../utils/utils";
44

5-
export class DerivationsSets {
6-
list: IDerivation[] = [];
7-
toDelete: IDerivation[] = [];
8-
9-
get length() {
10-
return this.list.length - this.toDelete.length;
11-
}
12-
13-
clearDeleted() {
14-
const toDelete = this.toDelete;
15-
if (toDelete.length !== 0) {
16-
const list = this.list;
17-
for (let i = 0; i < toDelete.length; i++) {
18-
toDelete[i].diffValue++;
19-
}
20-
21-
let i0 = 0;
22-
for (let i = 0; i < list.length; i++) {
23-
if (list[i].diffValue === 0) {
24-
if (i !== i0) list[i0] = list[i];
25-
i0++;
26-
} else {
27-
list[i].diffValue--;
28-
}
29-
}
30-
31-
// invariant(toDelete.every(a => a.diffValue === 0), "INTERNAL ERROR, clear deleted should leave diffValue everywhere 0");
32-
this.toDelete = [];
33-
list.length = i0;
34-
}
35-
}
36-
37-
asArray() {
38-
this.clearDeleted();
39-
return this.list;
40-
}
41-
42-
add(value: IDerivation) {
43-
// invariant(value.dependenciesState !== -1, "INTERNAL ERROR, can add only dependenciesState !== -1");
44-
this.list.push(value);
45-
}
46-
47-
remove(value: IDerivation) {
48-
// invariant(globalState.inBatch > 0, "INTERNAL ERROR, remove should be called only inside batch");
49-
if (this.toDelete.length === 0) {
50-
globalState.pendingDeletions.push(this);
51-
}
52-
this.toDelete.push(value);
53-
}
54-
// move(targetState: number, value: IDerivation) {
55-
// const m = value.__mapid;
56-
// delete this["data" + value.dependenciesState][m];
57-
// this["data" + targetState][m] = value;
58-
// }
5+
export interface LegacyObservers {
6+
asArray(): IDerivation[];
7+
length: number;
598
}
609

6110
export interface IDepTreeNode {
6211
name: string;
63-
observers?: DerivationsSets;
12+
observers?: LegacyObservers;
6413
observing?: IObservable[];
6514
}
6615

@@ -73,26 +22,85 @@ export interface IObservable extends IDepTreeNode {
7322
*/
7423
lastAccessedBy: number;
7524

76-
lowestObserverState: number;
25+
lowestObserverState: number; // to not repeat same propagations, see `invariantLOS`
7726
isPendingUnobservation: boolean; // for effective unobserving
7827
isObserved: boolean; // for unobserving only once ber observation
7928
// sets of observers grouped their state to only notify about changes.
80-
observers: DerivationsSets;
29+
observers: LegacyObservers;
30+
_observers: IDerivation[]; // a is observer if it occurs in _observers one time more than in _observersToDelete
31+
_observersToDelete: IDerivation[]; // must be empty when nothing is running
8132

8233
onBecomeUnobserved();
8334
}
8435

36+
export function legacyObservers(observable: IObservable): LegacyObservers {
37+
return {
38+
get length() {
39+
return observable._observers.length - observable._observersToDelete.length;
40+
},
41+
asArray() {
42+
return getObservers(observable);
43+
}
44+
};
45+
}
46+
47+
export function isObjectObservable(arg: any): arg is IObservable {
48+
return arg ? arg.lastAccessedBy !== undefined : false;
49+
}
50+
51+
export function hasObservers(observable: IObservable): boolean {
52+
return observable._observers.length - observable._observersToDelete.length > 0;
53+
}
54+
55+
// the trick is to only clear deleted elements when observers will be read whole anyway or when batch ends
56+
function clearDeletedObservers(observable: IObservable) {
57+
const _observersToDelete = observable._observersToDelete;
58+
if (_observersToDelete.length !== 0) {
59+
const _observers = observable._observers;
60+
for (let i = 0; i < _observersToDelete.length; i++) {
61+
_observersToDelete[i].diffValue++;
62+
}
63+
64+
let i0 = 0;
65+
for (let i = 0; i < _observers.length; i++) {
66+
if (_observers[i].diffValue === 0) {
67+
if (i !== i0) _observers[i0] = _observers[i];
68+
i0++;
69+
} else {
70+
_observers[i].diffValue--;
71+
}
72+
}
73+
74+
// invariant(_observersToDelete.every(a => a.diffValue === 0), "INTERNAL ERROR, clear deleted should leave diffValue everywhere 0");
75+
observable._observersToDelete = [];
76+
_observers.length = i0;
77+
}
78+
}
79+
80+
export function getObservers(observable: IObservable): IDerivation[] {
81+
clearDeletedObservers(observable);
82+
return observable._observers;
83+
}
84+
8585
export function addObserver(observable: IObservable, node: IDerivation) {
86-
observable.observers.add(node);
87-
observable.lowestObserverState = Math.min(observable.lowestObserverState, node.dependenciesState);
86+
// invariant(node.dependenciesState !== -1, "INTERNAL ERROR, can add only dependenciesState !== -1");
87+
observable._observers.push(node);
88+
89+
// observable.lowestObserverState = Math.min(observable.lowestObserverState, node.dependenciesState);
90+
if (observable.lowestObserverState > node.dependenciesState) observable.lowestObserverState = node.dependenciesState;
8891
if (!observable.isObserved) {
8992
observable.isObserved = true;
9093
}
9194
}
9295

9396
export function removeObserver(observable: IObservable, node: IDerivation) {
94-
observable.observers.remove(node);
95-
if (observable.isObserved && observable.observers.length === 0) {
97+
// invariant(globalState.inBatch > 0, "INTERNAL ERROR, remove should be called only inside batch");
98+
if (observable._observersToDelete.length === 0) {
99+
globalState.pendingDeletions.push(observable);
100+
}
101+
observable._observersToDelete.push(node);
102+
103+
if (observable.isObserved && !hasObservers(observable)) {
96104
if (globalState.inBatch > 0) {
97105
/**
98106
* Wan't to observe/unobserve max once per observable during batch.
@@ -119,12 +127,12 @@ export function endBatch() {
119127
while (globalState.pendingUnobservations.length > 0) {
120128
globalState.pendingUnobservations.splice(0).forEach(observable => {
121129
observable.isPendingUnobservation = false;
122-
if (observable.isObserved && observable.observers.length === 0) {
130+
if (observable.isObserved && !hasObservers(observable)) {
123131
observable.isObserved = false;
124132
observable.onBecomeUnobserved(); // TODO: test if this happens only once, e.g. remove returns bool!
125133
}
126134
});
127-
globalState.pendingDeletions.splice(0).forEach(d => d.clearDeleted());
135+
globalState.pendingDeletions.splice(0).forEach(clearDeletedObservers);
128136
}
129137
globalState.inBatch = 0;
130138
}
@@ -157,9 +165,8 @@ export function reportObserved(observable: IObservable) {
157165
}
158166

159167
function invariantLOS(observable: IObservable, msg) {
160-
console.log("check invariantLOS for", msg);
161-
const min = observable.observers.asArray().reduce((a, b) => Math.min(a, b.dependenciesState), 2);
162-
if (min >= observable.lowestObserverState) return;
168+
const min = getObservers(observable).reduce((a, b) => Math.min(a, b.dependenciesState), 2);
169+
if (min >= observable.lowestObserverState) return; // <- the only assumption about `lowestObserverState`
163170
throw new Error("lowestObserverState is wrong for " + msg + " because " + min + " < " + observable.lowestObserverState);
164171
}
165172

@@ -168,7 +175,7 @@ export function propagateChanged(observable: IObservable) {
168175
if (observable.lowestObserverState === 2) return;
169176
observable.lowestObserverState = 2;
170177

171-
const observers = observable.observers.asArray();
178+
const observers = getObservers(observable);
172179
for (let i = 0; i < observers.length; i++) {
173180
const d = observers[i];
174181
if (d.dependenciesState === 0) {
@@ -184,7 +191,7 @@ export function propagateChangeConfirmed(observable: IObservable) {
184191
if (observable.lowestObserverState === 2) return;
185192
observable.lowestObserverState = 2;
186193

187-
const observers = observable.observers.asArray();
194+
const observers = getObservers(observable);
188195
for (let i = 0; i < observers.length; i++) {
189196
const d = observers[i];
190197
if (d.dependenciesState === 1) {
@@ -201,7 +208,7 @@ export function propagateMaybeChanged(observable: IObservable) {
201208
if (observable.lowestObserverState !== 0) return;
202209
observable.lowestObserverState = 1;
203210

204-
const observers = observable.observers.asArray();
211+
const observers = getObservers(observable);
205212
for (let i = 0; i < observers.length; i++) {
206213
const d = observers[i];
207214
if (d.dependenciesState === 0) {

src/mobx.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {registerGlobals} from "./core/globalstate";
2020
registerGlobals();
2121

2222
export { IAtom, Atom, BaseAtom } from "./core/atom";
23-
export { IObservable, IDepTreeNode, DerivationsSets } from "./core/observable";
23+
export { IObservable, IDepTreeNode, LegacyObservers } from "./core/observable";
2424
export { Reaction, IReactionPublic } from "./core/reaction";
2525
export { IDerivation, untracked } from "./core/derivation";
2626
export { action, useStrict, isAction, runInAction } from "./core/action";

test/api.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ test('correct api should be exposed', function(t) {
55
t.deepEquals(Object.keys(mobx).sort(), [
66
'Atom',
77
'BaseAtom', // TODO: remove somehow
8-
'DerivationsSets', // TODO: remove somehow
98
'ObservableMap',
109
'Reaction',
1110
'SimpleEventEmitter',

0 commit comments

Comments
 (0)