1 | import {
|
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 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | export interface IReactionPublic {
|
44 | dispose(): void
|
45 | trace(enterBreakPoint?: boolean): void
|
46 | }
|
47 |
|
48 | export interface IReactionDisposer {
|
49 | (): void
|
50 | $mobx: Reaction
|
51 | }
|
52 |
|
53 | export class Reaction implements IDerivation, IReactionPublic {
|
54 | observing_: IObservable[] = []
|
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 |
|
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 |
|
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 |
|
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
|
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 |
|
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 |
|
168 | } else if (__DEV__) console.warn(`[mobx] (error in reaction '${this.name_}' suppressed, fix error of causing action below)`)
|
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 |
|
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 |
|
209 | export 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 |
|
219 |
|
220 |
|
221 |
|
222 | const MAX_REACTION_ITERATIONS = 100
|
223 |
|
224 | let reactionScheduler: (fn: () => void) => void = f => f()
|
225 |
|
226 | export function runReactions() {
|
227 |
|
228 | if (globalState.inBatch > 0 || globalState.isRunningReactions) return
|
229 | reactionScheduler(runReactionsHelper)
|
230 | }
|
231 |
|
232 | function runReactionsHelper() {
|
233 | globalState.isRunningReactions = true
|
234 | const allReactions = globalState.pendingReactions
|
235 | let iterations = 0
|
236 |
|
237 |
|
238 |
|
239 |
|
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)
|
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 |
|
257 | export const isReaction = createInstanceofPredicate("Reaction", Reaction)
|
258 |
|
259 | export function setReactionScheduler(fn: (f: () => void) => void) {
|
260 | const baseScheduler = reactionScheduler
|
261 | reactionScheduler = f => fn(() => baseScheduler(f))
|
262 | }
|