UNPKG

12.4 kBPlain TextView Raw
1import {
2 IAtom,
3 IDepTreeNode,
4 IObservable,
5 addObserver,
6 globalState,
7 isComputedValue,
8 removeObserver
9} from "../internal"
10
11export enum IDerivationState_ {
12 // before being run or (outside batch and not being observed)
13 // at this point derivation is not holding any data about dependency tree
14 NOT_TRACKING_ = -1,
15 // no shallow dependency changed since last computation
16 // won't recalculate derivation
17 // this is what makes mobx fast
18 UP_TO_DATE_ = 0,
19 // some deep dependency changed, but don't know if shallow dependency changed
20 // will require to check first if UP_TO_DATE or POSSIBLY_STALE
21 // currently only ComputedValue will propagate POSSIBLY_STALE
22 //
23 // having this state is second big optimization:
24 // don't have to recompute on every dependency change, but only when it's needed
25 POSSIBLY_STALE_ = 1,
26 // A shallow dependency has changed since last computation and the derivation
27 // will need to recompute when it's needed next.
28 STALE_ = 2
29}
30
31export enum TraceMode {
32 NONE,
33 LOG,
34 BREAK
35}
36
37/**
38 * A derivation is everything that can be derived from the state (all the atoms) in a pure manner.
39 * See https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254#.xvbh6qd74
40 */
41export interface IDerivation extends IDepTreeNode {
42 observing_: IObservable[]
43 newObserving_: null | IObservable[]
44 dependenciesState_: IDerivationState_
45 /**
46 * Id of the current run of a derivation. Each time the derivation is tracked
47 * this number is increased by one. This number is globally unique
48 */
49 runId_: number
50 /**
51 * amount of dependencies used by the derivation in this run, which has not been bound yet.
52 */
53 unboundDepsCount_: number
54 onBecomeStale_(): void
55 isTracing_: TraceMode
56
57 /**
58 * warn if the derivation has no dependencies after creation/update
59 */
60 requiresObservable_?: boolean
61}
62
63export class CaughtException {
64 constructor(public cause: any) {
65 // Empty
66 }
67}
68
69export function isCaughtException(e: any): e is CaughtException {
70 return e instanceof CaughtException
71}
72
73/**
74 * Finds out whether any dependency of the derivation has actually changed.
75 * If dependenciesState is 1 then it will recalculate dependencies,
76 * if any dependency changed it will propagate it by changing dependenciesState to 2.
77 *
78 * By iterating over the dependencies in the same order that they were reported and
79 * stopping on the first change, all the recalculations are only called for ComputedValues
80 * that will be tracked by derivation. That is because we assume that if the first x
81 * dependencies of the derivation doesn't change then the derivation should run the same way
82 * up until accessing x-th dependency.
83 */
84export function shouldCompute(derivation: IDerivation): boolean {
85 switch (derivation.dependenciesState_) {
86 case IDerivationState_.UP_TO_DATE_:
87 return false
88 case IDerivationState_.NOT_TRACKING_:
89 case IDerivationState_.STALE_:
90 return true
91 case IDerivationState_.POSSIBLY_STALE_: {
92 // state propagation can occur outside of action/reactive context #2195
93 const prevAllowStateReads = allowStateReadsStart(true)
94 const prevUntracked = untrackedStart() // no need for those computeds to be reported, they will be picked up in trackDerivedFunction.
95 const obs = derivation.observing_,
96 l = obs.length
97 for (let i = 0; i < l; i++) {
98 const obj = obs[i]
99 if (isComputedValue(obj)) {
100 if (globalState.disableErrorBoundaries) {
101 obj.get()
102 } else {
103 try {
104 obj.get()
105 } catch (e) {
106 // we are not interested in the value *or* exception at this moment, but if there is one, notify all
107 untrackedEnd(prevUntracked)
108 allowStateReadsEnd(prevAllowStateReads)
109 return true
110 }
111 }
112 // if ComputedValue `obj` actually changed it will be computed and propagated to its observers.
113 // and `derivation` is an observer of `obj`
114 // invariantShouldCompute(derivation)
115 if ((derivation.dependenciesState_ as any) === IDerivationState_.STALE_) {
116 untrackedEnd(prevUntracked)
117 allowStateReadsEnd(prevAllowStateReads)
118 return true
119 }
120 }
121 }
122 changeDependenciesStateTo0(derivation)
123 untrackedEnd(prevUntracked)
124 allowStateReadsEnd(prevAllowStateReads)
125 return false
126 }
127 }
128}
129
130export function isComputingDerivation() {
131 return globalState.trackingDerivation !== null // filter out actions inside computations
132}
133
134export function checkIfStateModificationsAreAllowed(atom: IAtom) {
135 if (!__DEV__) {
136 return
137 }
138 const hasObservers = atom.observers_.size > 0
139 // Should not be possible to change observed state outside strict mode, except during initialization, see #563
140 if (!globalState.allowStateChanges && (hasObservers || globalState.enforceActions === "always"))
141 console.warn(
142 "[MobX] " +
143 (globalState.enforceActions
144 ? "Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: "
145 : "Side effects like changing state are not allowed at this point. Are you trying to modify state from, for example, a computed value or the render function of a React component? You can wrap side effects in 'runInAction' (or decorate functions with 'action') if needed. Tried to modify: ") +
146 atom.name_
147 )
148}
149
150export function checkIfStateReadsAreAllowed(observable: IObservable) {
151 if (__DEV__ && !globalState.allowStateReads && globalState.observableRequiresReaction) {
152 console.warn(
153 `[mobx] Observable '${observable.name_}' being read outside a reactive context.`
154 )
155 }
156}
157
158/**
159 * Executes the provided function `f` and tracks which observables are being accessed.
160 * The tracking information is stored on the `derivation` object and the derivation is registered
161 * as observer of any of the accessed observables.
162 */
163export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
164 const prevAllowStateReads = allowStateReadsStart(true)
165 // pre allocate array allocation + room for variation in deps
166 // array will be trimmed by bindDependencies
167 changeDependenciesStateTo0(derivation)
168 derivation.newObserving_ = new Array(derivation.observing_.length + 100)
169 derivation.unboundDepsCount_ = 0
170 derivation.runId_ = ++globalState.runId
171 const prevTracking = globalState.trackingDerivation
172 globalState.trackingDerivation = derivation
173 globalState.inBatch++
174 let result
175 if (globalState.disableErrorBoundaries === true) {
176 result = f.call(context)
177 } else {
178 try {
179 result = f.call(context)
180 } catch (e) {
181 result = new CaughtException(e)
182 }
183 }
184 globalState.inBatch--
185 globalState.trackingDerivation = prevTracking
186 bindDependencies(derivation)
187
188 warnAboutDerivationWithoutDependencies(derivation)
189 allowStateReadsEnd(prevAllowStateReads)
190 return result
191}
192
193function warnAboutDerivationWithoutDependencies(derivation: IDerivation) {
194 if (!__DEV__) return
195
196 if (derivation.observing_.length !== 0) return
197
198 if (globalState.reactionRequiresObservable || derivation.requiresObservable_) {
199 console.warn(
200 `[mobx] Derivation '${derivation.name_}' is created/updated without reading any observable value.`
201 )
202 }
203}
204
205/**
206 * diffs newObserving with observing.
207 * update observing to be newObserving with unique observables
208 * notify observers that become observed/unobserved
209 */
210function bindDependencies(derivation: IDerivation) {
211 // invariant(derivation.dependenciesState !== IDerivationState.NOT_TRACKING, "INTERNAL ERROR bindDependencies expects derivation.dependenciesState !== -1");
212 const prevObserving = derivation.observing_
213 const observing = (derivation.observing_ = derivation.newObserving_!)
214 let lowestNewObservingDerivationState = IDerivationState_.UP_TO_DATE_
215
216 // Go through all new observables and check diffValue: (this list can contain duplicates):
217 // 0: first occurrence, change to 1 and keep it
218 // 1: extra occurrence, drop it
219 let i0 = 0,
220 l = derivation.unboundDepsCount_
221 for (let i = 0; i < l; i++) {
222 const dep = observing[i]
223 if (dep.diffValue_ === 0) {
224 dep.diffValue_ = 1
225 if (i0 !== i) observing[i0] = dep
226 i0++
227 }
228
229 // Upcast is 'safe' here, because if dep is IObservable, `dependenciesState` will be undefined,
230 // not hitting the condition
231 if (((dep as any) as IDerivation).dependenciesState_ > lowestNewObservingDerivationState) {
232 lowestNewObservingDerivationState = ((dep as any) as IDerivation).dependenciesState_
233 }
234 }
235 observing.length = i0
236
237 derivation.newObserving_ = null // newObserving shouldn't be needed outside tracking (statement moved down to work around FF bug, see #614)
238
239 // Go through all old observables and check diffValue: (it is unique after last bindDependencies)
240 // 0: it's not in new observables, unobserve it
241 // 1: it keeps being observed, don't want to notify it. change to 0
242 l = prevObserving.length
243 while (l--) {
244 const dep = prevObserving[l]
245 if (dep.diffValue_ === 0) {
246 removeObserver(dep, derivation)
247 }
248 dep.diffValue_ = 0
249 }
250
251 // Go through all new observables and check diffValue: (now it should be unique)
252 // 0: it was set to 0 in last loop. don't need to do anything.
253 // 1: it wasn't observed, let's observe it. set back to 0
254 while (i0--) {
255 const dep = observing[i0]
256 if (dep.diffValue_ === 1) {
257 dep.diffValue_ = 0
258 addObserver(dep, derivation)
259 }
260 }
261
262 // Some new observed derivations may become stale during this derivation computation
263 // so they have had no chance to propagate staleness (#916)
264 if (lowestNewObservingDerivationState !== IDerivationState_.UP_TO_DATE_) {
265 derivation.dependenciesState_ = lowestNewObservingDerivationState
266 derivation.onBecomeStale_()
267 }
268}
269
270export function clearObserving(derivation: IDerivation) {
271 // invariant(globalState.inBatch > 0, "INTERNAL ERROR clearObserving should be called only inside batch");
272 const obs = derivation.observing_
273 derivation.observing_ = []
274 let i = obs.length
275 while (i--) removeObserver(obs[i], derivation)
276
277 derivation.dependenciesState_ = IDerivationState_.NOT_TRACKING_
278}
279
280export function untracked<T>(action: () => T): T {
281 const prev = untrackedStart()
282 try {
283 return action()
284 } finally {
285 untrackedEnd(prev)
286 }
287}
288
289export function untrackedStart(): IDerivation | null {
290 const prev = globalState.trackingDerivation
291 globalState.trackingDerivation = null
292 return prev
293}
294
295export function untrackedEnd(prev: IDerivation | null) {
296 globalState.trackingDerivation = prev
297}
298
299export function allowStateReadsStart(allowStateReads: boolean) {
300 const prev = globalState.allowStateReads
301 globalState.allowStateReads = allowStateReads
302 return prev
303}
304
305export function allowStateReadsEnd(prev: boolean) {
306 globalState.allowStateReads = prev
307}
308
309/**
310 * needed to keep `lowestObserverState` correct. when changing from (2 or 1) to 0
311 *
312 */
313export function changeDependenciesStateTo0(derivation: IDerivation) {
314 if (derivation.dependenciesState_ === IDerivationState_.UP_TO_DATE_) return
315 derivation.dependenciesState_ = IDerivationState_.UP_TO_DATE_
316
317 const obs = derivation.observing_
318 let i = obs.length
319 while (i--) obs[i].lowestObserverState_ = IDerivationState_.UP_TO_DATE_
320}