-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
derivation.ts
236 lines (214 loc) · 7.99 KB
/
derivation.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import {IObservable, IDepTreeNode, addObserver, removeObserver} from "./observable";
import {globalState} from "./globalstate";
import {fail} from "../utils/utils";
import {isComputedValue} from "./computedvalue";
import {getMessage} from "../utils/messages";
export enum IDerivationState {
// before being run or (outside batch and not being observed)
// at this point derivation is not holding any data about dependency tree
NOT_TRACKING = -1,
// no shallow dependency changed since last computation
// won't recalculate derivation
// this is what makes mobx fast
UP_TO_DATE = 0,
// some deep dependency changed, but don't know if shallow dependency changed
// will require to check first if UP_TO_DATE or POSSIBLY_STALE
// currently only ComputedValue will propagate POSSIBLY_STALE
//
// having this state is second big optimization:
// don't have to recompute on every dependency change, but only when it's needed
POSSIBLY_STALE = 1,
// shallow dependency changed
// will need to recompute when it's needed
STALE = 2
}
/**
* A derivation is everything that can be derived from the state (all the atoms) in a pure manner.
* See https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254#.xvbh6qd74
*/
export interface IDerivation extends IDepTreeNode {
observing: IObservable[];
newObserving: null | IObservable[];
dependenciesState: IDerivationState;
/**
* Id of the current run of a derivation. Each time the derivation is tracked
* this number is increased by one. This number is globally unique
*/
runId: number;
/**
* amount of dependencies used by the derivation in this run, which has not been bound yet.
*/
unboundDepsCount: number;
__mapid: string;
onBecomeStale();
}
export class CaughtException {
constructor(public cause: any) {
// Empty
}
}
export function isCaughtException(e): e is CaughtException {
return e instanceof CaughtException;
}
/**
* Finds out wether any dependency of derivation actually changed
* If dependenciesState is 1 it will recalculate dependencies,
* if any dependency changed it will propagate it by changing dependenciesState to 2.
*
* By iterating over dependencies in the same order they were reported and stoping on first change
* all recalculations are called only for ComputedValues that will be tracked anyway by derivation.
* That is because we assume that if first x dependencies of derivation doesn't change
* than derivation shuold run the same way up until accessing x-th dependency.
*/
export function shouldCompute(derivation: IDerivation): boolean {
switch (derivation.dependenciesState) {
case IDerivationState.UP_TO_DATE: return false;
case IDerivationState.NOT_TRACKING: case IDerivationState.STALE: return true;
case IDerivationState.POSSIBLY_STALE: {
const prevUntracked = untrackedStart(); // no need for those computeds to be reported, they will be picked up in trackDerivedFunction.
const obs = derivation.observing, l = obs.length;
for (let i = 0; i < l; i++) {
const obj = obs[i];
if (isComputedValue(obj)) {
try {
obj.get();
} catch (e) {
// we are not interested in the value *or* exception at this moment, but if there is one, notify all
untrackedEnd(prevUntracked);
return true;
}
// if ComputedValue `obj` actually changed it will be computed and propagated to its observers.
// and `derivation` is an observer of `obj`
if ((derivation as any).dependenciesState === IDerivationState.STALE) {
untrackedEnd(prevUntracked);
return true;
}
}
}
changeDependenciesStateTo0(derivation);
untrackedEnd(prevUntracked);
return false;
}
}
}
export function isComputingDerivation() {
return globalState.trackingDerivation !== null; // filter out actions inside computations
}
export function checkIfStateModificationsAreAllowed(atom: IObservable) {
if (globalState.allowStateChanges === false) {
if (globalState.strictMode)
fail(getMessage("m030") + atom.name);
else
fail(getMessage("m031") + atom.name);
} else {
// Observed observables should not be modified during a computation,
// even not from inside an action!
if (globalState.computationDepth > 0 && atom.observers.length > 0) {
fail(getMessage("m027") + atom.name);
}
}
}
/**
* Executes the provided function `f` and tracks which observables are being accessed.
* The tracking information is stored on the `derivation` object and the derivation is registered
* as observer of any of the accessed observables.
*/
export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context) {
// pre allocate array allocation + room for variation in deps
// array will be trimmed by bindDependencies
changeDependenciesStateTo0(derivation);
derivation.newObserving = new Array(derivation.observing.length + 100);
derivation.unboundDepsCount = 0;
derivation.runId = ++globalState.runId;
const prevTracking = globalState.trackingDerivation;
globalState.trackingDerivation = derivation;
let result;
try {
result = f.call(context);
} catch (e) {
result = new CaughtException(e);
}
globalState.trackingDerivation = prevTracking;
bindDependencies(derivation);
return result;
}
/**
* diffs newObserving with obsering.
* update observing to be newObserving with unique observables
* notify observers that become observed/unobserved
*/
function bindDependencies(derivation: IDerivation) {
// invariant(derivation.dependenciesState !== IDerivationState.NOT_TRACKING, "INTERNAL ERROR bindDependencies expects derivation.dependenciesState !== -1");
const prevObserving = derivation.observing;
const observing = derivation.observing = derivation.newObserving!;
derivation.newObserving = null; // newObserving shouldn't be needed outside tracking
// Go through all new observables and check diffValue: (this list can contain duplicates):
// 0: first occurence, change to 1 and keep it
// 1: extra occurence, drop it
let i0 = 0, l = derivation.unboundDepsCount;
for (let i = 0; i < l; i++) {
const dep = observing[i];
if (dep.diffValue === 0) {
dep.diffValue = 1;
if (i0 !== i) observing[i0] = dep;
i0++;
}
}
observing.length = i0;
// Go through all old observables and check diffValue: (it is unique after last bindDependencies)
// 0: it's not in new observables, unobserve it
// 1: it keeps being observed, don't want to notify it. change to 0
l = prevObserving.length;
while (l--) {
const dep = prevObserving[l];
if (dep.diffValue === 0) {
removeObserver(dep, derivation);
}
dep.diffValue = 0;
}
// Go through all new observables and check diffValue: (now it should be unique)
// 0: it was set to 0 in last loop. don't need to do anything.
// 1: it wasn't observed, let's observe it. set back to 0
while (i0--) {
const dep = observing[i0];
if (dep.diffValue === 1) {
dep.diffValue = 0;
addObserver(dep, derivation);
}
}
}
export function clearObserving(derivation: IDerivation) {
// invariant(globalState.inBatch > 0, "INTERNAL ERROR clearObserving should be called only inside batch");
const obs = derivation.observing;
let i = obs.length;
while (i--)
removeObserver(obs[i], derivation);
derivation.dependenciesState = IDerivationState.NOT_TRACKING;
obs.length = 0;
}
export function untracked<T>(action: () => T): T {
const prev = untrackedStart();
const res = action();
untrackedEnd(prev);
return res;
}
export function untrackedStart(): IDerivation | null {
const prev = globalState.trackingDerivation;
globalState.trackingDerivation = null;
return prev;
}
export function untrackedEnd(prev: IDerivation | null) {
globalState.trackingDerivation = prev;
}
/**
* needed to keep `lowestObserverState` correct. when changing from (2 or 1) to 0
*
*/
export function changeDependenciesStateTo0(derivation: IDerivation) {
if (derivation.dependenciesState === IDerivationState.UP_TO_DATE) return;
derivation.dependenciesState = IDerivationState.UP_TO_DATE;
const obs = derivation.observing;
let i = obs.length;
while (i--)
obs[i].lowestObserverState = IDerivationState.UP_TO_DATE;
}