UNPKG

12.7 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 (
141 !globalState.allowStateChanges &&
142 (hasObservers || globalState.enforceActions === "always")
143 ) {
144 console.warn(
145 "[MobX] " +
146 (globalState.enforceActions
147 ? "Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: "
148 : "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: ") +
149 atom.name_
150 )
151 }
152}
153
154export function checkIfStateReadsAreAllowed(observable: IObservable) {
155 if (__DEV__ && !globalState.allowStateReads && globalState.observableRequiresReaction) {
156 console.warn(
157 `[mobx] Observable '${observable.name_}' being read outside a reactive context.`
158 )
159 }
160}
161
162/**
163 * Executes the provided function `f` and tracks which observables are being accessed.
164 * The tracking information is stored on the `derivation` object and the derivation is registered
165 * as observer of any of the accessed observables.
166 */
167export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
168 const prevAllowStateReads = allowStateReadsStart(true)
169 changeDependenciesStateTo0(derivation)
170 // Preallocate array; will be trimmed by bindDependencies.
171 derivation.newObserving_ = new Array(
172 // Reserve constant space for initial dependencies, dynamic space otherwise.
173 // See https://github.com/mobxjs/mobx/pull/3833
174 derivation.runId_ === 0 ? 100 : derivation.observing_.length
175 )
176 derivation.unboundDepsCount_ = 0
177 derivation.runId_ = ++globalState.runId
178 const prevTracking = globalState.trackingDerivation
179 globalState.trackingDerivation = derivation
180 globalState.inBatch++
181 let result
182 if (globalState.disableErrorBoundaries === true) {
183 result = f.call(context)
184 } else {
185 try {
186 result = f.call(context)
187 } catch (e) {
188 result = new CaughtException(e)
189 }
190 }
191 globalState.inBatch--
192 globalState.trackingDerivation = prevTracking
193 bindDependencies(derivation)
194
195 warnAboutDerivationWithoutDependencies(derivation)
196 allowStateReadsEnd(prevAllowStateReads)
197 return result
198}
199
200function warnAboutDerivationWithoutDependencies(derivation: IDerivation) {
201 if (!__DEV__) {
202 return
203 }
204
205 if (derivation.observing_.length !== 0) {
206 return
207 }
208
209 if (
210 typeof derivation.requiresObservable_ === "boolean"
211 ? derivation.requiresObservable_
212 : globalState.reactionRequiresObservable
213 ) {
214 console.warn(
215 `[mobx] Derivation '${derivation.name_}' is created/updated without reading any observable value.`
216 )
217 }
218}
219
220/**
221 * diffs newObserving with observing.
222 * update observing to be newObserving with unique observables
223 * notify observers that become observed/unobserved
224 */
225function bindDependencies(derivation: IDerivation) {
226 // invariant(derivation.dependenciesState !== IDerivationState.NOT_TRACKING, "INTERNAL ERROR bindDependencies expects derivation.dependenciesState !== -1");
227 const prevObserving = derivation.observing_
228 const observing = (derivation.observing_ = derivation.newObserving_!)
229 let lowestNewObservingDerivationState = IDerivationState_.UP_TO_DATE_
230
231 // Go through all new observables and check diffValue: (this list can contain duplicates):
232 // 0: first occurrence, change to 1 and keep it
233 // 1: extra occurrence, drop it
234 let i0 = 0,
235 l = derivation.unboundDepsCount_
236 for (let i = 0; i < l; i++) {
237 const dep = observing[i]
238 if (dep.diffValue_ === 0) {
239 dep.diffValue_ = 1
240 if (i0 !== i) {
241 observing[i0] = dep
242 }
243 i0++
244 }
245
246 // Upcast is 'safe' here, because if dep is IObservable, `dependenciesState` will be undefined,
247 // not hitting the condition
248 if ((dep as any as IDerivation).dependenciesState_ > lowestNewObservingDerivationState) {
249 lowestNewObservingDerivationState = (dep as any as IDerivation).dependenciesState_
250 }
251 }
252 observing.length = i0
253
254 derivation.newObserving_ = null // newObserving shouldn't be needed outside tracking (statement moved down to work around FF bug, see #614)
255
256 // Go through all old observables and check diffValue: (it is unique after last bindDependencies)
257 // 0: it's not in new observables, unobserve it
258 // 1: it keeps being observed, don't want to notify it. change to 0
259 l = prevObserving.length
260 while (l--) {
261 const dep = prevObserving[l]
262 if (dep.diffValue_ === 0) {
263 removeObserver(dep, derivation)
264 }
265 dep.diffValue_ = 0
266 }
267
268 // Go through all new observables and check diffValue: (now it should be unique)
269 // 0: it was set to 0 in last loop. don't need to do anything.
270 // 1: it wasn't observed, let's observe it. set back to 0
271 while (i0--) {
272 const dep = observing[i0]
273 if (dep.diffValue_ === 1) {
274 dep.diffValue_ = 0
275 addObserver(dep, derivation)
276 }
277 }
278
279 // Some new observed derivations may become stale during this derivation computation
280 // so they have had no chance to propagate staleness (#916)
281 if (lowestNewObservingDerivationState !== IDerivationState_.UP_TO_DATE_) {
282 derivation.dependenciesState_ = lowestNewObservingDerivationState
283 derivation.onBecomeStale_()
284 }
285}
286
287export function clearObserving(derivation: IDerivation) {
288 // invariant(globalState.inBatch > 0, "INTERNAL ERROR clearObserving should be called only inside batch");
289 const obs = derivation.observing_
290 derivation.observing_ = []
291 let i = obs.length
292 while (i--) {
293 removeObserver(obs[i], derivation)
294 }
295
296 derivation.dependenciesState_ = IDerivationState_.NOT_TRACKING_
297}
298
299export function untracked<T>(action: () => T): T {
300 const prev = untrackedStart()
301 try {
302 return action()
303 } finally {
304 untrackedEnd(prev)
305 }
306}
307
308export function untrackedStart(): IDerivation | null {
309 const prev = globalState.trackingDerivation
310 globalState.trackingDerivation = null
311 return prev
312}
313
314export function untrackedEnd(prev: IDerivation | null) {
315 globalState.trackingDerivation = prev
316}
317
318export function allowStateReadsStart(allowStateReads: boolean) {
319 const prev = globalState.allowStateReads
320 globalState.allowStateReads = allowStateReads
321 return prev
322}
323
324export function allowStateReadsEnd(prev: boolean) {
325 globalState.allowStateReads = prev
326}
327
328/**
329 * needed to keep `lowestObserverState` correct. when changing from (2 or 1) to 0
330 *
331 */
332export function changeDependenciesStateTo0(derivation: IDerivation) {
333 if (derivation.dependenciesState_ === IDerivationState_.UP_TO_DATE_) {
334 return
335 }
336 derivation.dependenciesState_ = IDerivationState_.UP_TO_DATE_
337
338 const obs = derivation.observing_
339 let i = obs.length
340 while (i--) {
341 obs[i].lowestObserverState_ = IDerivationState_.UP_TO_DATE_
342 }
343}