-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
reaction.ts
274 lines (243 loc) · 8.78 KB
/
reaction.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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
import {
IDerivation,
IDerivationState,
trackDerivedFunction,
clearObserving,
shouldCompute,
isCaughtException,
TraceMode
} from "./derivation"
import { IObservable, startBatch, endBatch } from "./observable"
import { globalState } from "./globalstate"
import {
createInstanceofPredicate,
getNextId,
invariant,
unique,
joinStrings
} from "../utils/utils"
import { isSpyEnabled, spyReport, spyReportStart, spyReportEnd } from "./spy"
import { getMessage } from "../utils/messages"
import { trace } from "../api/whyrun"
/**
* Reactions are a special kind of derivations. Several things distinguishes them from normal reactive computations
*
* 1) They will always run, whether they are used by other computations or not.
* This means that they are very suitable for triggering side effects like logging, updating the DOM and making network requests.
* 2) They are not observable themselves
* 3) They will always run after any 'normal' derivations
* 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.
*
* The state machine of a Reaction is as follows:
*
* 1) after creating, the reaction should be started by calling `runReaction` or by scheduling it (see also `autorun`)
* 2) the `onInvalidate` handler should somehow result in a call to `this.track(someFunction)`
* 3) all observables accessed in `someFunction` will be observed by this reaction.
* 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
* 5) `onInvalidate` will be called, and we are back at step 1.
*
*/
export interface IReactionPublic {
dispose(): void
trace(enterBreakPoint?: boolean): void
}
export interface IReactionDisposer {
(): void
$mobx: Reaction
onError(handler: (error: any, derivation: IDerivation) => void)
}
export class Reaction implements IDerivation, IReactionPublic {
observing: IObservable[] = [] // nodes we are looking at. Our value depends on these nodes
newObserving: IObservable[] = []
dependenciesState = IDerivationState.NOT_TRACKING
diffValue = 0
runId = 0
unboundDepsCount = 0
__mapid = "#" + getNextId()
isDisposed = false
_isScheduled = false
_isTrackPending = false
_isRunning = false
isTracing: TraceMode = TraceMode.NONE
errorHandler: (error: any, derivation: IDerivation) => void
constructor(
public name: string = "Reaction@" + getNextId(),
private onInvalidate: () => void
) {}
onBecomeStale() {
this.schedule()
}
schedule() {
if (!this._isScheduled) {
this._isScheduled = true
globalState.pendingReactions.push(this)
runReactions()
}
}
isScheduled() {
return this._isScheduled
}
/**
* internal, use schedule() if you intend to kick off a reaction
*/
runReaction() {
if (!this.isDisposed) {
startBatch()
this._isScheduled = false
if (shouldCompute(this)) {
this._isTrackPending = true
this.onInvalidate()
if (this._isTrackPending && isSpyEnabled()) {
// onInvalidate didn't trigger track right away..
spyReport({
object: this,
type: "scheduled-reaction"
})
}
}
endBatch()
}
}
track(fn: () => void) {
startBatch()
const notify = isSpyEnabled()
let startTime
if (notify) {
startTime = Date.now()
spyReportStart({
object: this,
type: "reaction",
fn
})
}
this._isRunning = true
const result = trackDerivedFunction(this, fn, undefined)
this._isRunning = false
this._isTrackPending = false
if (this.isDisposed) {
// disposed during last run. Clean up everything that was bound after the dispose call.
clearObserving(this)
}
if (isCaughtException(result)) this.reportExceptionInDerivation(result.cause)
if (notify) {
spyReportEnd({
time: Date.now() - startTime
})
}
endBatch()
}
reportExceptionInDerivation(error: any) {
if (this.errorHandler) {
this.errorHandler(error, this)
return
}
const message = `[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: '${this}`
const messageToUser = getMessage("m037")
console.error(
message || messageToUser /* latter will not be true, make sure uglify doesn't remove */,
error
)
/** If debugging brought you here, please, read the above message :-). Tnx! */
if (isSpyEnabled()) {
spyReport({
type: "error",
message,
error,
object: this
})
}
globalState.globalReactionErrorHandlers.forEach(f => f(error, this))
}
dispose() {
if (!this.isDisposed) {
this.isDisposed = true
if (!this._isRunning) {
// if disposed while running, clean up later. Maybe not optimal, but rare case
startBatch()
clearObserving(this)
endBatch()
}
}
}
getDisposer(): IReactionDisposer {
const r = this.dispose.bind(this)
r.$mobx = this
r.onError = registerErrorHandler
return r
}
toString() {
return `Reaction[${this.name}]`
}
whyRun() {
const observing = unique(this._isRunning ? this.newObserving : this.observing).map(
dep => dep.name
)
return `
WhyRun? reaction '${this.name}':
* Status: [${this.isDisposed
? "stopped"
: this._isRunning ? "running" : this.isScheduled() ? "scheduled" : "idle"}]
* This reaction will re-run if any of the following observables changes:
${joinStrings(observing)}
${this._isRunning
? " (... or any observable accessed during the remainder of the current run)"
: ""}
${getMessage("m038")}
`
}
trace(enterBreakPoint: boolean = false) {
trace(this, enterBreakPoint)
}
}
function registerErrorHandler(handler) {
invariant(this && this.$mobx && isReaction(this.$mobx), "Invalid `this`")
invariant(!this.$mobx.errorHandler, "Only one onErrorHandler can be registered")
this.$mobx.errorHandler = handler
}
export function onReactionError(
handler: (error: any, derivation: IDerivation) => void
): () => void {
globalState.globalReactionErrorHandlers.push(handler)
return () => {
const idx = globalState.globalReactionErrorHandlers.indexOf(handler)
if (idx >= 0) globalState.globalReactionErrorHandlers.splice(idx, 1)
}
}
/**
* Magic number alert!
* Defines within how many times a reaction is allowed to re-trigger itself
* until it is assumed that this is gonna be a never ending loop...
*/
const MAX_REACTION_ITERATIONS = 100
let reactionScheduler: (fn: () => void) => void = f => f()
export function runReactions() {
// Trampolining, if runReactions are already running, new reactions will be picked up
if (globalState.inBatch > 0 || globalState.isRunningReactions) return
reactionScheduler(runReactionsHelper)
}
function runReactionsHelper() {
globalState.isRunningReactions = true
const allReactions = globalState.pendingReactions
let iterations = 0
// While running reactions, new reactions might be triggered.
// Hence we work with two variables and check whether
// we converge to no remaining reactions after a while.
while (allReactions.length > 0) {
if (++iterations === MAX_REACTION_ITERATIONS) {
console.error(
`Reaction doesn't converge to a stable state after ${MAX_REACTION_ITERATIONS} iterations.` +
` Probably there is a cycle in the reactive function: ${allReactions[0]}`
)
allReactions.splice(0) // clear reactions
}
let remainingReactions = allReactions.splice(0)
for (let i = 0, l = remainingReactions.length; i < l; i++)
remainingReactions[i].runReaction()
}
globalState.isRunningReactions = false
}
export const isReaction = createInstanceofPredicate("Reaction", Reaction)
export function setReactionScheduler(fn: (f: () => void) => void) {
const baseScheduler = reactionScheduler
reactionScheduler = f => fn(() => baseScheduler(f))
}