UNPKG

mobx

Version:

Simple, scalable state management.

277 lines (247 loc) 9.28 kB
import { $mobx, IDerivation, IDerivationState_, IObservable, Lambda, TraceMode, clearObserving, createInstanceofPredicate, endBatch, getNextId, globalState, isCaughtException, isSpyEnabled, shouldCompute, spyReport, spyReportEnd, spyReportStart, startBatch, trace, trackDerivedFunction, GenericAbortSignal } from "../internal" /** * 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 } 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 isDisposed_ = false isScheduled_ = false isTrackPending_ = false isRunning_ = false isTracing_: TraceMode = TraceMode.NONE constructor( public name_: string = __DEV__ ? "Reaction@" + getNextId() : "Reaction", private onInvalidate_: () => void, private errorHandler_?: (error: any, derivation: IDerivation) => void, public requiresObservable_? ) {} 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 const prev = globalState.trackingContext globalState.trackingContext = this if (shouldCompute(this)) { this.isTrackPending_ = true try { this.onInvalidate_() if (__DEV__ && this.isTrackPending_ && isSpyEnabled()) { // onInvalidate didn't trigger track right away.. spyReport({ name: this.name_, type: "scheduled-reaction" }) } } catch (e) { this.reportExceptionInDerivation_(e) } } globalState.trackingContext = prev endBatch() } } track(fn: () => void) { if (this.isDisposed_) { return // console.warn("Reaction already disposed") // Note: Not a warning / error in mobx 4 either } startBatch() const notify = isSpyEnabled() let startTime if (__DEV__ && notify) { startTime = Date.now() spyReportStart({ name: this.name_, type: "reaction" }) } this.isRunning_ = true const prevReaction = globalState.trackingContext // reactions could create reactions... globalState.trackingContext = this const result = trackDerivedFunction(this, fn, undefined) globalState.trackingContext = prevReaction 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 (__DEV__ && notify) { spyReportEnd({ time: Date.now() - startTime }) } endBatch() } reportExceptionInDerivation_(error: any) { if (this.errorHandler_) { this.errorHandler_(error, this) return } if (globalState.disableErrorBoundaries) { throw error } const message = __DEV__ ? `[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: '${this}'` : `[mobx] uncaught error in '${this}'` if (!globalState.suppressReactionErrors) { console.error(message, error) /** If debugging brought you here, please, read the above message :-). Tnx! */ } else if (__DEV__) { console.warn(`[mobx] (error in reaction '${this.name_}' suppressed, fix error of causing action below)`) } // prettier-ignore if (__DEV__ && isSpyEnabled()) { spyReport({ type: "error", name: this.name_, message, error: "" + error }) } 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_(abortSignal?: GenericAbortSignal): IReactionDisposer { const dispose = (() => { this.dispose() abortSignal?.removeEventListener?.("abort", dispose) }) as IReactionDisposer abortSignal?.addEventListener?.("abort", dispose) dispose[$mobx] = this return dispose } toString() { return `Reaction[${this.name_}]` } trace(enterBreakPoint: boolean = false) { trace(this, enterBreakPoint) } } export function onReactionError(handler: (error: any, derivation: IDerivation) => void): Lambda { 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( __DEV__ ? `Reaction doesn't converge to a stable state after ${MAX_REACTION_ITERATIONS} iterations.` + ` Probably there is a cycle in the reactive function: ${allReactions[0]}` : `[mobx] cycle in reaction: ${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)) }