diff --git a/.changeset/shaggy-monkeys-cover.md b/.changeset/shaggy-monkeys-cover.md new file mode 100644 index 000000000..9513757cb --- /dev/null +++ b/.changeset/shaggy-monkeys-cover.md @@ -0,0 +1,5 @@ +--- +"mobx": patch +--- + +Shrink Atom and Reaction using a bitfield diff --git a/packages/mobx/__tests__/v4/base/observables.js b/packages/mobx/__tests__/v4/base/observables.js index 1bd486d60..c1e8d8088 100644 --- a/packages/mobx/__tests__/v4/base/observables.js +++ b/packages/mobx/__tests__/v4/base/observables.js @@ -1156,7 +1156,7 @@ test("forcefully tracked reaction should still yield valid results", function () transaction(function () { x.set(4) a.track(identity) - expect(a.isScheduled()).toBe(true) + expect(a.isScheduled).toBe(true) expect(z).toBe(4) expect(runCount).toBe(2) }) @@ -1166,17 +1166,17 @@ test("forcefully tracked reaction should still yield valid results", function () transaction(function () { x.set(5) - expect(a.isScheduled()).toBe(true) + expect(a.isScheduled).toBe(true) a.track(identity) expect(z).toBe(5) expect(runCount).toBe(3) - expect(a.isScheduled()).toBe(true) + expect(a.isScheduled).toBe(true) x.set(6) expect(z).toBe(5) expect(runCount).toBe(3) }) - expect(a.isScheduled()).toBe(false) + expect(a.isScheduled).toBe(false) expect(z).toBe(6) expect(runCount).toBe(4) }) diff --git a/packages/mobx/__tests__/v5/base/errorhandling.js b/packages/mobx/__tests__/v5/base/errorhandling.js index 6e2eb2dcd..81b9ab13f 100644 --- a/packages/mobx/__tests__/v5/base/errorhandling.js +++ b/packages/mobx/__tests__/v5/base/errorhandling.js @@ -485,7 +485,7 @@ test("peeking inside erroring computed value doesn't bork (global) state", () => expect(a.isPendingUnobservation).toBe(false) expect(a.observers_.size).toBe(0) - expect(a.diffValue_).toBe(0) + expect(a.diffValue).toBe(0) expect(a.lowestObserverState_).toBe(-1) expect(a.hasUnreportedChange_).toBe(false) expect(a.value_).toBe(1) @@ -495,7 +495,7 @@ test("peeking inside erroring computed value doesn't bork (global) state", () => expect(b.newObserving_).toBe(null) expect(b.isPendingUnobservation).toBe(false) expect(b.observers_.size).toBe(0) - expect(b.diffValue_).toBe(0) + expect(b.diffValue).toBe(0) expect(b.lowestObserverState_).toBe(0) expect(b.unboundDepsCount_).toBe(0) expect(() => { @@ -523,7 +523,7 @@ describe("peeking inside autorun doesn't bork (global) state", () => { test("it should update correctly initially", () => { expect(a.isPendingUnobservation).toBe(false) expect(a.observers_.size).toBe(1) - expect(a.diffValue_).toBe(0) + expect(a.diffValue).toBe(0) expect(a.lowestObserverState_).toBe(-1) expect(a.hasUnreportedChange_).toBe(false) expect(a.value_).toBe(1) @@ -533,7 +533,7 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(b.newObserving_).toBe(null) expect(b.isPendingUnobservation).toBe(false) expect(b.observers_.size).toBe(1) - expect(b.diffValue_).toBe(0) + expect(b.diffValue).toBe(0) expect(b.lowestObserverState_).toBe(0) expect(b.unboundDepsCount_).toBe(1) // value is always the last bound amount of observers expect(b.value_).toBe(1) @@ -542,12 +542,12 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(c.dependenciesState_).toBe(0) expect(c.observing_.length).toBe(1) expect(c.newObserving_).toBe(null) - expect(c.diffValue_).toBe(0) + expect(c.diffValue).toBe(0) expect(c.unboundDepsCount_).toBe(1) - expect(c.isDisposed_).toBe(false) - expect(c.isScheduled_).toBe(false) - expect(c.isTrackPending_).toBe(false) - expect(c.isRunning_).toBe(false) + expect(c.isDisposed).toBe(false) + expect(c.isScheduled).toBe(false) + expect(c.isTrackPending).toBe(false) + expect(c.isRunning).toBe(false) checkGlobalState() }) @@ -560,7 +560,7 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(a.isPendingUnobservation).toBe(false) expect(a.observers_.size).toBe(1) - expect(a.diffValue_).toBe(0) + expect(a.diffValue).toBe(0) expect(a.lowestObserverState_).toBe(0) expect(a.hasUnreportedChange_).toBe(false) expect(a.value_).toBe(2) @@ -570,7 +570,7 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(b.newObserving_).toBe(null) expect(b.isPendingUnobservation).toBe(false) expect(b.observers_.size).toBe(1) - expect(b.diffValue_).toBe(0) + expect(b.diffValue).toBe(0) expect(b.lowestObserverState_).toBe(0) expect(b.unboundDepsCount_).toBe(1) expect(b.isComputing).toBe(false) @@ -579,12 +579,12 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(c.dependenciesState_).toBe(0) expect(c.observing_.length).toBe(1) expect(c.newObserving_).toBe(null) - expect(c.diffValue_).toBe(0) + expect(c.diffValue).toBe(0) expect(c.unboundDepsCount_).toBe(1) - expect(c.isDisposed_).toBe(false) - expect(c.isScheduled_).toBe(false) - expect(c.isTrackPending_).toBe(false) - expect(c.isRunning_).toBe(false) + expect(c.isDisposed).toBe(false) + expect(c.isScheduled).toBe(false) + expect(c.isTrackPending).toBe(false) + expect(c.isRunning).toBe(false) checkGlobalState() }) @@ -596,7 +596,7 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(a.isPendingUnobservation).toBe(false) expect(a.observers_.size).toBe(1) - expect(a.diffValue_).toBe(0) + expect(a.diffValue).toBe(0) expect(a.lowestObserverState_).toBe(0) expect(a.hasUnreportedChange_).toBe(false) expect(a.value_).toBe(3) @@ -606,7 +606,7 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(b.newObserving_).toBe(null) expect(b.isPendingUnobservation).toBe(false) expect(b.observers_.size).toBe(1) - expect(b.diffValue_).toBe(0) + expect(b.diffValue).toBe(0) expect(b.lowestObserverState_).toBe(0) expect(b.unboundDepsCount_).toBe(1) expect(b.value_).toBe(3) @@ -615,12 +615,12 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(c.dependenciesState_).toBe(0) expect(c.observing_.length).toBe(1) expect(c.newObserving_).toBe(null) - expect(c.diffValue_).toBe(0) + expect(c.diffValue).toBe(0) expect(c.unboundDepsCount_).toBe(1) - expect(c.isDisposed_).toBe(false) - expect(c.isScheduled_).toBe(false) - expect(c.isTrackPending_).toBe(false) - expect(c.isRunning_).toBe(false) + expect(c.isDisposed).toBe(false) + expect(c.isScheduled).toBe(false) + expect(c.isTrackPending).toBe(false) + expect(c.isRunning).toBe(false) checkGlobalState() }) @@ -630,7 +630,7 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(a.isPendingUnobservation).toBe(false) expect(a.observers_.size).toBe(0) - expect(a.diffValue_).toBe(0) + expect(a.diffValue).toBe(0) expect(a.lowestObserverState_).toBe(0) expect(a.hasUnreportedChange_).toBe(false) expect(a.value_).toBe(3) @@ -640,7 +640,7 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(b.newObserving_).toBe(null) expect(b.isPendingUnobservation).toBe(false) expect(b.observers_.size).toBe(0) - expect(b.diffValue_).toBe(0) + expect(b.diffValue).toBe(0) expect(b.lowestObserverState_).toBe(0) expect(b.unboundDepsCount_).toBe(1) expect(b.value_).not.toBe(3) @@ -649,12 +649,12 @@ describe("peeking inside autorun doesn't bork (global) state", () => { expect(c.dependenciesState_).toBe(-1) expect(c.observing_.length).toBe(0) expect(c.newObserving_).toBe(null) - expect(c.diffValue_).toBe(0) + expect(c.diffValue).toBe(0) expect(c.unboundDepsCount_).toBe(1) - expect(c.isDisposed_).toBe(true) - expect(c.isScheduled_).toBe(false) - expect(c.isTrackPending_).toBe(false) - expect(c.isRunning_).toBe(false) + expect(c.isDisposed).toBe(true) + expect(c.isScheduled).toBe(false) + expect(c.isTrackPending).toBe(false) + expect(c.isRunning).toBe(false) expect(b.get()).toBe(3) diff --git a/packages/mobx/__tests__/v5/base/observables.js b/packages/mobx/__tests__/v5/base/observables.js index 24d229ac1..63bf54825 100644 --- a/packages/mobx/__tests__/v5/base/observables.js +++ b/packages/mobx/__tests__/v5/base/observables.js @@ -1197,7 +1197,7 @@ test("forcefully tracked reaction should still yield valid results", function () transaction(function () { x.set(4) a.track(identity) - expect(a.isScheduled()).toBe(true) + expect(a.isScheduled).toBe(true) expect(z).toBe(4) expect(runCount).toBe(2) }) @@ -1207,17 +1207,17 @@ test("forcefully tracked reaction should still yield valid results", function () transaction(function () { x.set(5) - expect(a.isScheduled()).toBe(true) + expect(a.isScheduled).toBe(true) a.track(identity) expect(z).toBe(5) expect(runCount).toBe(3) - expect(a.isScheduled()).toBe(true) + expect(a.isScheduled).toBe(true) x.set(6) expect(z).toBe(5) expect(runCount).toBe(3) }) - expect(a.isScheduled()).toBe(false) + expect(a.isScheduled).toBe(false) expect(z).toBe(6) expect(runCount).toBe(4) }) diff --git a/packages/mobx/src/api/autorun.ts b/packages/mobx/src/api/autorun.ts index e4ba669ad..63454ab16 100644 --- a/packages/mobx/src/api/autorun.ts +++ b/packages/mobx/src/api/autorun.ts @@ -75,7 +75,7 @@ export function autorun( isScheduled = true scheduler(() => { isScheduled = false - if (!reaction.isDisposed_) { + if (!reaction.isDisposed) { reaction.track(reactionRunner) } }) @@ -90,7 +90,7 @@ export function autorun( view(reaction) } - if(!opts?.signal?.aborted) { + if (!opts?.signal?.aborted) { reaction.schedule_() } return reaction.getDisposer_(opts?.signal) @@ -160,7 +160,7 @@ export function reaction( function reactionRunner() { isScheduled = false - if (r.isDisposed_) { + if (r.isDisposed) { return } let changed: boolean = false @@ -181,7 +181,7 @@ export function reaction( firstTime = false } - if(!opts?.signal?.aborted) { + if (!opts?.signal?.aborted) { r.schedule_() } return r.getDisposer_(opts?.signal) diff --git a/packages/mobx/src/api/when.ts b/packages/mobx/src/api/when.ts index 6417aebb9..66985a1af 100644 --- a/packages/mobx/src/api/when.ts +++ b/packages/mobx/src/api/when.ts @@ -38,7 +38,7 @@ function _when(predicate: () => boolean, effect: Lambda, opts: IWhenOptions): IR if (typeof opts.timeout === "number") { const error = new Error("WHEN_TIMEOUT") timeoutHandle = setTimeout(() => { - if (!disposer[$mobx].isDisposed_) { + if (!disposer[$mobx].isDisposed) { disposer() if (opts.onError) { opts.onError(error) diff --git a/packages/mobx/src/core/atom.ts b/packages/mobx/src/core/atom.ts index 6c1788fb3..2df57def5 100644 --- a/packages/mobx/src/core/atom.ts +++ b/packages/mobx/src/core/atom.ts @@ -14,6 +14,8 @@ import { Lambda } from "../internal" +import { getFlag, setFlag } from "../utils/utils" + export const $mobx = Symbol("mobx administration") export interface IAtom extends IObservable { @@ -22,11 +24,13 @@ export interface IAtom extends IObservable { } export class Atom implements IAtom { - isPendingUnobservation = false // for effective unobserving. BaseAtom has true, for extra optimization, so its onBecomeUnobserved never gets called, because it's not needed - isBeingObserved = false + private static readonly isBeingObservedMask_ = 0b001 + private static readonly isPendingUnobservationMask_ = 0b010 + private static readonly diffValueMask_ = 0b100 + private flags_ = 0b000 + observers_ = new Set() - diffValue_ = 0 lastAccessedBy_ = 0 lowestObserverState_ = IDerivationState_.NOT_TRACKING_ /** @@ -35,6 +39,28 @@ export class Atom implements IAtom { */ constructor(public name_ = __DEV__ ? "Atom@" + getNextId() : "Atom") {} + // for effective unobserving. BaseAtom has true, for extra optimization, so its onBecomeUnobserved never gets called, because it's not needed + get isBeingObserved(): boolean { + return getFlag(this.flags_, Atom.isBeingObservedMask_) + } + set isBeingObserved(newValue: boolean) { + this.flags_ = setFlag(this.flags_, Atom.isBeingObservedMask_, newValue) + } + + get isPendingUnobservation(): boolean { + return getFlag(this.flags_, Atom.isPendingUnobservationMask_) + } + set isPendingUnobservation(newValue: boolean) { + this.flags_ = setFlag(this.flags_, Atom.isPendingUnobservationMask_, newValue) + } + + get diffValue(): 0 | 1 { + return getFlag(this.flags_, Atom.diffValueMask_) ? 1 : 0 + } + set diffValue(newValue: 0 | 1) { + this.flags_ = setFlag(this.flags_, Atom.diffValueMask_, newValue === 1 ? true : false) + } + // onBecomeObservedListeners public onBOL: Set | undefined // onBecomeUnobservedListeners diff --git a/packages/mobx/src/core/computedvalue.ts b/packages/mobx/src/core/computedvalue.ts index 1a6a6bf6e..cec036d6c 100644 --- a/packages/mobx/src/core/computedvalue.ts +++ b/packages/mobx/src/core/computedvalue.ts @@ -32,6 +32,8 @@ import { allowStateChangesEnd } from "../internal" +import { getFlag, setFlag } from "../utils/utils" + export interface IComputedValue { get(): T set(value: T): void @@ -56,18 +58,6 @@ export type IComputedDidChange = { oldValue: T | undefined } -function getFlag(flags: number, mask: number) { - return !!(flags & mask) -} -function setFlag(flags: number, mask: number, newValue: boolean): number { - if (newValue) { - flags |= mask - } else { - flags &= ~mask - } - return flags -} - /** * A node in the state dependency root that observes other nodes, and can be observed itself. * @@ -92,7 +82,6 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva observing_: IObservable[] = [] // nodes we are looking at. Our value depends on these nodes newObserving_ = null // during tracking it's an array with new observed observers observers_ = new Set() - diffValue_ = 0 runId_ = 0 lastAccessedBy_ = 0 lowestObserverState_ = IDerivationState_.UP_TO_DATE_ @@ -101,11 +90,12 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva name_: string triggeredBy_?: string - private static readonly isComputingMask_ = 0b0001 - private static readonly isRunningSetterMask_ = 0b0010 - private static readonly isBeingObservedMask_ = 0b0100 - private static readonly isPendingUnobservationMask_ = 0b1000 - private flags_ = 0b0000 + private static readonly isComputingMask_ = 0b00001 + private static readonly isRunningSetterMask_ = 0b00010 + private static readonly isBeingObservedMask_ = 0b00100 + private static readonly isPendingUnobservationMask_ = 0b01000 + private static readonly diffValueMask_ = 0b10000 + private flags_ = 0b00000 derivation: () => T // N.B: unminified as it is used by MST setter_?: (value: T) => void @@ -197,6 +187,17 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva this.flags_ = setFlag(this.flags_, ComputedValue.isPendingUnobservationMask_, newValue) } + get diffValue(): 0 | 1 { + return getFlag(this.flags_, ComputedValue.diffValueMask_) ? 1 : 0 + } + set diffValue(newValue: 0 | 1) { + this.flags_ = setFlag( + this.flags_, + ComputedValue.diffValueMask_, + newValue === 1 ? true : false + ) + } + /** * Returns the current value of this computed value. * Will evaluate its computation first if needed. diff --git a/packages/mobx/src/core/derivation.ts b/packages/mobx/src/core/derivation.ts index 252d18601..b1c2d2504 100644 --- a/packages/mobx/src/core/derivation.ts +++ b/packages/mobx/src/core/derivation.ts @@ -235,8 +235,8 @@ function bindDependencies(derivation: IDerivation) { l = derivation.unboundDepsCount_ for (let i = 0; i < l; i++) { const dep = observing[i] - if (dep.diffValue_ === 0) { - dep.diffValue_ = 1 + if (dep.diffValue === 0) { + dep.diffValue = 1 if (i0 !== i) { observing[i0] = dep } @@ -259,10 +259,10 @@ function bindDependencies(derivation: IDerivation) { l = prevObserving.length while (l--) { const dep = prevObserving[l] - if (dep.diffValue_ === 0) { + if (dep.diffValue === 0) { removeObserver(dep, derivation) } - dep.diffValue_ = 0 + dep.diffValue = 0 } // Go through all new observables and check diffValue: (now it should be unique) @@ -270,8 +270,8 @@ function bindDependencies(derivation: IDerivation) { // 1: it wasn't observed, let's observe it. set back to 0 while (i0--) { const dep = observing[i0] - if (dep.diffValue_ === 1) { - dep.diffValue_ = 0 + if (dep.diffValue === 1) { + dep.diffValue = 0 addObserver(dep, derivation) } } diff --git a/packages/mobx/src/core/observable.ts b/packages/mobx/src/core/observable.ts index 71bd85311..5ddbc74c8 100644 --- a/packages/mobx/src/core/observable.ts +++ b/packages/mobx/src/core/observable.ts @@ -17,7 +17,7 @@ export interface IDepTreeNode { } export interface IObservable extends IDepTreeNode { - diffValue_: number + diffValue: number /** * Id of the derivation *run* that last accessed this observable. * If this id equals the *run* id of the current derivation, diff --git a/packages/mobx/src/core/reaction.ts b/packages/mobx/src/core/reaction.ts index 7472091f8..177a47455 100644 --- a/packages/mobx/src/core/reaction.ts +++ b/packages/mobx/src/core/reaction.ts @@ -22,6 +22,8 @@ import { GenericAbortSignal } from "../internal" +import { getFlag, setFlag } from "../utils/utils" + /** * Reactions are a special kind of derivations. Several things distinguishes them from normal reactive computations * @@ -55,13 +57,16 @@ 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 + + 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( @@ -71,37 +76,68 @@ export class Reaction implements IDerivation, IReactionPublic { 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 + 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_) { + if (!this.isDisposed) { startBatch() - this.isScheduled_ = false + this.isScheduled = false const prev = globalState.trackingContext globalState.trackingContext = this if (shouldCompute(this)) { - this.isTrackPending_ = true + this.isTrackPending = true try { this.onInvalidate_() - if (__DEV__ && this.isTrackPending_ && isSpyEnabled()) { + if (__DEV__ && this.isTrackPending && isSpyEnabled()) { // onInvalidate didn't trigger track right away.. spyReport({ name: this.name_, @@ -118,7 +154,7 @@ export class Reaction implements IDerivation, IReactionPublic { } track(fn: () => void) { - if (this.isDisposed_) { + if (this.isDisposed) { return // console.warn("Reaction already disposed") // Note: Not a warning / error in mobx 4 either } @@ -132,14 +168,14 @@ export class Reaction implements IDerivation, IReactionPublic { type: "reaction" }) } - this.isRunning_ = true + 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_) { + 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) } @@ -185,9 +221,9 @@ export class Reaction implements IDerivation, IReactionPublic { } dispose() { - if (!this.isDisposed_) { - this.isDisposed_ = true - if (!this.isRunning_) { + 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) diff --git a/packages/mobx/src/utils/utils.ts b/packages/mobx/src/utils/utils.ts index 1f1ca701d..f4ac2137c 100644 --- a/packages/mobx/src/utils/utils.ts +++ b/packages/mobx/src/utils/utils.ts @@ -212,3 +212,16 @@ export const getOwnPropertyDescriptors = }) return res } + +export function getFlag(flags: number, mask: number) { + return !!(flags & mask) +} + +export function setFlag(flags: number, mask: number, newValue: boolean): number { + if (newValue) { + flags |= mask + } else { + flags &= ~mask + } + return flags +}