Skip to content

Commit

Permalink
Shrink Atom and Reaction using a bitfield (mobxjs#3901)
Browse files Browse the repository at this point in the history
  • Loading branch information
peterm-canva authored and taj-p committed Aug 30, 2024
1 parent a171ab1 commit 01fa73b
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 94 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-monkeys-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"mobx": patch
---

Shrink Atom and Reaction using a bitfield
8 changes: 4 additions & 4 deletions packages/mobx/__tests__/v4/base/observables.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand All @@ -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)
})
Expand Down
60 changes: 30 additions & 30 deletions packages/mobx/__tests__/v5/base/errorhandling.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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()
})

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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()
})

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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()
})
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)

Expand Down
8 changes: 4 additions & 4 deletions packages/mobx/__tests__/v5/base/observables.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand All @@ -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)
})
Expand Down
8 changes: 4 additions & 4 deletions packages/mobx/src/api/autorun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function autorun(
isScheduled = true
scheduler(() => {
isScheduled = false
if (!reaction.isDisposed_) {
if (!reaction.isDisposed) {
reaction.track(reactionRunner)
}
})
Expand All @@ -90,7 +90,7 @@ export function autorun(
view(reaction)
}

if(!opts?.signal?.aborted) {
if (!opts?.signal?.aborted) {
reaction.schedule_()
}
return reaction.getDisposer_(opts?.signal)
Expand Down Expand Up @@ -160,7 +160,7 @@ export function reaction<T, FireImmediately extends boolean = false>(

function reactionRunner() {
isScheduled = false
if (r.isDisposed_) {
if (r.isDisposed) {
return
}
let changed: boolean = false
Expand All @@ -181,7 +181,7 @@ export function reaction<T, FireImmediately extends boolean = false>(
firstTime = false
}

if(!opts?.signal?.aborted) {
if (!opts?.signal?.aborted) {
r.schedule_()
}
return r.getDisposer_(opts?.signal)
Expand Down
2 changes: 1 addition & 1 deletion packages/mobx/src/api/when.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 29 additions & 3 deletions packages/mobx/src/core/atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<IDerivation>()

diffValue_ = 0
lastAccessedBy_ = 0
lowestObserverState_ = IDerivationState_.NOT_TRACKING_
/**
Expand All @@ -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<Lambda> | undefined
// onBecomeUnobservedListeners
Expand Down
37 changes: 19 additions & 18 deletions packages/mobx/src/core/computedvalue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
allowStateChangesEnd
} from "../internal"

import { getFlag, setFlag } from "../utils/utils"

export interface IComputedValue<T> {
get(): T
set(value: T): void
Expand All @@ -56,18 +58,6 @@ export type IComputedDidChange<T = any> = {
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.
*
Expand All @@ -92,7 +82,6 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, 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<IDerivation>()
diffValue_ = 0
runId_ = 0
lastAccessedBy_ = 0
lowestObserverState_ = IDerivationState_.UP_TO_DATE_
Expand All @@ -101,11 +90,12 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, 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
Expand Down Expand Up @@ -197,6 +187,17 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, 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.
Expand Down
Loading

0 comments on commit 01fa73b

Please sign in to comment.