UNPKG

8.97 kBPlain TextView Raw
1import {
2 $mobx,
3 IDerivation,
4 IDerivationState_,
5 IObservable,
6 Lambda,
7 TraceMode,
8 clearObserving,
9 createInstanceofPredicate,
10 endBatch,
11 getNextId,
12 globalState,
13 isCaughtException,
14 isSpyEnabled,
15 shouldCompute,
16 spyReport,
17 spyReportEnd,
18 spyReportStart,
19 startBatch,
20 trace,
21 trackDerivedFunction
22} from "../internal"
23
24/**
25 * Reactions are a special kind of derivations. Several things distinguishes them from normal reactive computations
26 *
27 * 1) They will always run, whether they are used by other computations or not.
28 * This means that they are very suitable for triggering side effects like logging, updating the DOM and making network requests.
29 * 2) They are not observable themselves
30 * 3) They will always run after any 'normal' derivations
31 * 4) They are allowed to change the state and thereby triggering themselves again, as long as they make sure the state propagates to a stable state in a reasonable amount of iterations.
32 *
33 * The state machine of a Reaction is as follows:
34 *
35 * 1) after creating, the reaction should be started by calling `runReaction` or by scheduling it (see also `autorun`)
36 * 2) the `onInvalidate` handler should somehow result in a call to `this.track(someFunction)`
37 * 3) all observables accessed in `someFunction` will be observed by this reaction.
38 * 4) as soon as some of the dependencies has changed the Reaction will be rescheduled for another run (after the current mutation or transaction). `isScheduled` will yield true once a dependency is stale and during this period
39 * 5) `onInvalidate` will be called, and we are back at step 1.
40 *
41 */
42
43export interface IReactionPublic {
44 dispose(): void
45 trace(enterBreakPoint?: boolean): void
46}
47
48export interface IReactionDisposer {
49 (): void
50 $mobx: Reaction
51}
52
53export class Reaction implements IDerivation, IReactionPublic {
54 observing_: IObservable[] = [] // nodes we are looking at. Our value depends on these nodes
55 newObserving_: IObservable[] = []
56 dependenciesState_ = IDerivationState_.NOT_TRACKING_
57 diffValue_ = 0
58 runId_ = 0
59 unboundDepsCount_ = 0
60 isDisposed_ = false
61 isScheduled_ = false
62 isTrackPending_ = false
63 isRunning_ = false
64 isTracing_: TraceMode = TraceMode.NONE
65
66 constructor(
67 public name_: string = __DEV__ ? "Reaction@" + getNextId() : "Reaction",
68 private onInvalidate_: () => void,
69 private errorHandler_?: (error: any, derivation: IDerivation) => void,
70 public requiresObservable_ = false
71 ) {}
72
73 onBecomeStale_() {
74 this.schedule_()
75 }
76
77 schedule_() {
78 if (!this.isScheduled_) {
79 this.isScheduled_ = true
80 globalState.pendingReactions.push(this)
81 runReactions()
82 }
83 }
84
85 isScheduled() {
86 return this.isScheduled_
87 }
88
89 /**
90 * internal, use schedule() if you intend to kick off a reaction
91 */
92 runReaction_() {
93 if (!this.isDisposed_) {
94 startBatch()
95 this.isScheduled_ = false
96 const prev = globalState.trackingContext
97 globalState.trackingContext = this
98 if (shouldCompute(this)) {
99 this.isTrackPending_ = true
100
101 try {
102 this.onInvalidate_()
103 if (__DEV__ && this.isTrackPending_ && isSpyEnabled()) {
104 // onInvalidate didn't trigger track right away..
105 spyReport({
106 name: this.name_,
107 type: "scheduled-reaction"
108 })
109 }
110 } catch (e) {
111 this.reportExceptionInDerivation_(e)
112 }
113 }
114 globalState.trackingContext = prev
115 endBatch()
116 }
117 }
118
119 track(fn: () => void) {
120 if (this.isDisposed_) {
121 return
122 // console.warn("Reaction already disposed") // Note: Not a warning / error in mobx 4 either
123 }
124 startBatch()
125 const notify = isSpyEnabled()
126 let startTime
127 if (__DEV__ && notify) {
128 startTime = Date.now()
129 spyReportStart({
130 name: this.name_,
131 type: "reaction"
132 })
133 }
134 this.isRunning_ = true
135 const prevReaction = globalState.trackingContext // reactions could create reactions...
136 globalState.trackingContext = this
137 const result = trackDerivedFunction(this, fn, undefined)
138 globalState.trackingContext = prevReaction
139 this.isRunning_ = false
140 this.isTrackPending_ = false
141 if (this.isDisposed_) {
142 // disposed during last run. Clean up everything that was bound after the dispose call.
143 clearObserving(this)
144 }
145 if (isCaughtException(result)) this.reportExceptionInDerivation_(result.cause)
146 if (__DEV__ && notify) {
147 spyReportEnd({
148 time: Date.now() - startTime
149 })
150 }
151 endBatch()
152 }
153
154 reportExceptionInDerivation_(error: any) {
155 if (this.errorHandler_) {
156 this.errorHandler_(error, this)
157 return
158 }
159
160 if (globalState.disableErrorBoundaries) throw error
161
162 const message = __DEV__
163 ? `[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: '${this}'`
164 : `[mobx] uncaught error in '${this}'`
165 if (!globalState.suppressReactionErrors) {
166 console.error(message, error)
167 /** If debugging brought you here, please, read the above message :-). Tnx! */
168 } else if (__DEV__) console.warn(`[mobx] (error in reaction '${this.name_}' suppressed, fix error of causing action below)`) // prettier-ignore
169
170 if (__DEV__ && isSpyEnabled()) {
171 spyReport({
172 type: "error",
173 name: this.name_,
174 message,
175 error: "" + error
176 })
177 }
178
179 globalState.globalReactionErrorHandlers.forEach(f => f(error, this))
180 }
181
182 dispose() {
183 if (!this.isDisposed_) {
184 this.isDisposed_ = true
185 if (!this.isRunning_) {
186 // if disposed while running, clean up later. Maybe not optimal, but rare case
187 startBatch()
188 clearObserving(this)
189 endBatch()
190 }
191 }
192 }
193
194 getDisposer_(): IReactionDisposer {
195 const r = this.dispose.bind(this) as IReactionDisposer
196 r[$mobx] = this
197 return r
198 }
199
200 toString() {
201 return `Reaction[${this.name_}]`
202 }
203
204 trace(enterBreakPoint: boolean = false) {
205 trace(this, enterBreakPoint)
206 }
207}
208
209export function onReactionError(handler: (error: any, derivation: IDerivation) => void): Lambda {
210 globalState.globalReactionErrorHandlers.push(handler)
211 return () => {
212 const idx = globalState.globalReactionErrorHandlers.indexOf(handler)
213 if (idx >= 0) globalState.globalReactionErrorHandlers.splice(idx, 1)
214 }
215}
216
217/**
218 * Magic number alert!
219 * Defines within how many times a reaction is allowed to re-trigger itself
220 * until it is assumed that this is gonna be a never ending loop...
221 */
222const MAX_REACTION_ITERATIONS = 100
223
224let reactionScheduler: (fn: () => void) => void = f => f()
225
226export function runReactions() {
227 // Trampolining, if runReactions are already running, new reactions will be picked up
228 if (globalState.inBatch > 0 || globalState.isRunningReactions) return
229 reactionScheduler(runReactionsHelper)
230}
231
232function runReactionsHelper() {
233 globalState.isRunningReactions = true
234 const allReactions = globalState.pendingReactions
235 let iterations = 0
236
237 // While running reactions, new reactions might be triggered.
238 // Hence we work with two variables and check whether
239 // we converge to no remaining reactions after a while.
240 while (allReactions.length > 0) {
241 if (++iterations === MAX_REACTION_ITERATIONS) {
242 console.error(
243 __DEV__
244 ? `Reaction doesn't converge to a stable state after ${MAX_REACTION_ITERATIONS} iterations.` +
245 ` Probably there is a cycle in the reactive function: ${allReactions[0]}`
246 : `[mobx] cycle in reaction: ${allReactions[0]}`
247 )
248 allReactions.splice(0) // clear reactions
249 }
250 let remainingReactions = allReactions.splice(0)
251 for (let i = 0, l = remainingReactions.length; i < l; i++)
252 remainingReactions[i].runReaction_()
253 }
254 globalState.isRunningReactions = false
255}
256
257export const isReaction = createInstanceofPredicate("Reaction", Reaction)
258
259export function setReactionScheduler(fn: (f: () => void) => void) {
260 const baseScheduler = reactionScheduler
261 reactionScheduler = f => fn(() => baseScheduler(f))
262}