-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
derivation.ts
248 lines (225 loc) · 9.67 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
237
238
239
240
241
242
243
244
245
246
247
248
import { IObservable, IDepTreeNode, addObserver, removeObserver } from "./observable"
import { IAtom } from "./atom"
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,
// A shallow dependency has changed since last computation and the derivation
// will need to recompute when it's needed next.
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 whether any dependency of the derivation has actually changed.
* If dependenciesState is 1 then it will recalculate dependencies,
* if any dependency changed it will propagate it by changing dependenciesState to 2.
*
* By iterating over the dependencies in the same order that they were reported and
* stopping on the first change, all the recalculations are only called for ComputedValues
* that will be tracked by derivation. That is because we assume that if the first x
* dependencies of the derivation doesn't change then the derivation should 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: IAtom) {
const hasObservers = atom.observers.length > 0
// Should never be possible to change an observed observable from inside computed, see #798
if (globalState.computationDepth > 0 && hasObservers) fail(getMessage("m031") + atom.name)
// Should not be possible to change observed state outside strict mode, except during initialization, see #563
if (!globalState.allowStateChanges && hasObservers)
fail(getMessage(globalState.strictMode ? "m030a" : "m030b") + 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 observing.
* 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!)
let lowestNewObservingDerivationState = IDerivationState.UP_TO_DATE
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 occurrence, change to 1 and keep it
// 1: extra occurrence, 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++
}
// Upcast is 'safe' here, because if dep is IObservable, `dependenciesState` will be undefined,
// not hitting the condition
if (((dep as any) as IDerivation).dependenciesState > lowestNewObservingDerivationState) {
lowestNewObservingDerivationState = ((dep as any) as IDerivation).dependenciesState
}
}
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)
}
}
// Some new observed derivations may become stale during this derivation computation
// so they have had no chance to propagate staleness (#916)
if (lowestNewObservingDerivationState !== IDerivationState.UP_TO_DATE) {
derivation.dependenciesState = lowestNewObservingDerivationState
derivation.onBecomeStale()
}
}
export function clearObserving(derivation: IDerivation) {
// invariant(globalState.inBatch > 0, "INTERNAL ERROR clearObserving should be called only inside batch");
const obs = derivation.observing
derivation.observing = []
let i = obs.length
while (i--) removeObserver(obs[i], derivation)
derivation.dependenciesState = IDerivationState.NOT_TRACKING
}
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
}