UNPKG

mobx

Version:

Simple, scalable state management.

314 lines (277 loc) 10.6 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" import { getFlag, setFlag } from "../utils/utils" /** * 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_ runId_ = 0 unboundDepsCount_ = 0 private static readonly isDisposedMask_ = 0b00001 private static readonly isScheduledMask_ = 0b00010 private static readonly isTrackPendingMask_ = 0b00100 private static readonly isRunningMask_ = 0b01000 private static readonly diffValueMask_ = 0b10000 private flags_ = 0b00000 isTracing_: TraceMode = TraceMode.NONE constructor( public name_: string = __DEV__ ? "Reaction@" + getNextId() : "Reaction", private onInvalidate_: () => void, private errorHandler_?: (error: any, derivation: IDerivation) => void, public requiresObservable_? ) {} get isDisposed() { return getFlag(this.flags_, Reaction.isDisposedMask_) } set isDisposed(newValue: boolean) { this.flags_ = setFlag(this.flags_, Reaction.isDisposedMask_, newValue) } get isScheduled() { return getFlag(this.flags_, Reaction.isScheduledMask_) } set isScheduled(newValue: boolean) { this.flags_ = setFlag(this.flags_, Reaction.isScheduledMask_, newValue) } get isTrackPending() { return getFlag(this.flags_, Reaction.isTrackPendingMask_) } set isTrackPending(newValue: boolean) { this.flags_ = setFlag(this.flags_, Reaction.isTrackPendingMask_, newValue) } get isRunning() { return getFlag(this.flags_, Reaction.isRunningMask_) } set isRunning(newValue: boolean) { this.flags_ = setFlag(this.flags_, Reaction.isRunningMask_, newValue) } get diffValue(): 0 | 1 { return getFlag(this.flags_, Reaction.diffValueMask_) ? 1 : 0 } set diffValue(newValue: 0 | 1) { this.flags_ = setFlag(this.flags_, Reaction.diffValueMask_, newValue === 1 ? true : false) } onBecomeStale_() { this.schedule_() } schedule_() { if (!this.isScheduled) { this.isScheduled = true globalState.pendingReactions.push(this) runReactions() } } /** * 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)) }