Skip to content

Commit

Permalink
add support for WeakRefs
Browse files Browse the repository at this point in the history
  • Loading branch information
Matchlighter committed Feb 27, 2023
1 parent faf075c commit 1bf10cd
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 11 deletions.
4 changes: 4 additions & 0 deletions docs/computeds.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,7 @@ It is recommended to set this one to `true` on very expensive computed values. I
### `keepAlive`

This avoids suspending computed values when they are not being observed by anything (see the above explanation). Can potentially create memory leaks, similar to the ones discussed for [reactions](reactions.md#always-dispose-of-reactions).

### `weak`

Intended for use with `keepAlive`. When `true`, MobX will use a `WeakRef` (_if_ you're not targeting something old that doesn't support `WeakRef`s) when add the `computed` to any `observables`. If your reference to the `computed` is garbage collected, the `computed` will be too (instead of `observable`s holding references and preventing garbage collection)
128 changes: 128 additions & 0 deletions packages/mobx/__tests__/v5/base/weakset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
IObservableValue,
autorun,
computed,
observable,
onBecomeObserved,
onBecomeUnobserved,
runInAction
} from "../../../src/mobx"
const gc = require("expose-gc/function")

let events: string[] = []
beforeEach(() => {
events = []
})

function nextFrame() {
return new Promise(accept => setTimeout(accept, 1))
}

async function gc_cycle() {
await nextFrame()
gc()
await nextFrame()
}

test("observables should not hold a reference to weak reactions", async () => {
let x = 0
const o = observable.box(10)

;(() => {
const au = autorun(
() => {
x += o.get()
},
{ weak: true }
)

o.set(5)
expect(x).toEqual(15)
})()

await gc_cycle()
expect((o as any).observers_.size).toEqual(0)

o.set(20)
expect(x).toEqual(15)
})

test("observables should hold a reference to reactions", async () => {
let x = 0
const o = observable.box(10)
;(() => {
autorun(() => {
x += o.get()
}, {})

o.set(5)
})()

await gc_cycle()
expect((o as any).observers_.size).toEqual(1)

o.set(20)
expect(x).toEqual(35)
})

test("observables should not hold a reference to weak computeds", async () => {
const o = observable.box(10)
let wref
;(() => {
const kac = computed(
() => {
return o.get()
},
{ keepAlive: true, weak: true }
)
wref = new WeakRef(kac)
kac.get()
})()

expect(wref.deref()).not.toEqual(null)
await gc_cycle()
expect(wref.deref() == null).toBeTruthy()
expect((o as any).observers_.size).toEqual(0)
})

test("observables should hold a reference to computeds", async () => {
const o = observable.box(10)
let wref
;(() => {
const kac = computed(
() => {
return o.get()
},
{ keepAlive: true }
)
kac.get()
wref = new WeakRef(kac)
})()

expect(wref.deref() != null).toBeTruthy()
await nextFrame()
gc()
await nextFrame()
expect(wref.deref() != null).toBeTruthy()
expect((o as any).observers_.size).toEqual(1)
})

test("garbage collection should trigger onBOU", async () => {
const o = observable.box(10)

onBecomeObserved(o, () => events.push(`o observed`))
onBecomeUnobserved(o, () => events.push(`o unobserved`))

;(() => {
autorun(
() => {
o.get()
},
{ weak: true }
)
})()

expect(events).toEqual(["o observed"])
await gc_cycle()
expect(events).toEqual(["o observed", "o unobserved"])
})
3 changes: 2 additions & 1 deletion packages/mobx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"@babel/preset-typescript": "^7.9.0",
"@babel/runtime": "^7.9.2",
"conditional-type-checks": "^1.0.5",
"flow-bin": "^0.123.0"
"flow-bin": "^0.123.0",
"expose-gc": "^1.0.0"
},
"keywords": [
"mobx",
Expand Down
15 changes: 12 additions & 3 deletions packages/mobx/src/api/autorun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export interface IAutorunOptions {
requiresObservable?: boolean
scheduler?: (callback: () => void) => any
onError?: (error: any) => void
/**
* Observees will not prevent this Reaction from being garbage collected and disposed - you'll need to keep a reference to it somewhere
*
* This is an advanced feature that, in 99.99% of cases you won't need.
*/
weak?: boolean
}

/**
Expand Down Expand Up @@ -59,7 +65,8 @@ export function autorun(
this.track(reactionRunner)
},
opts.onError,
opts.requiresObservable
opts.requiresObservable,
opts.weak
)
} else {
const scheduler = createSchedulerFromOptions(opts)
Expand All @@ -80,7 +87,8 @@ export function autorun(
}
},
opts.onError,
opts.requiresObservable
opts.requiresObservable,
opts.weak
)
}

Expand Down Expand Up @@ -152,7 +160,8 @@ export function reaction<T, FireImmediately extends boolean = false>(
}
},
opts.onError,
opts.requiresObservable
opts.requiresObservable,
opts.weak
)

function reactionRunner() {
Expand Down
21 changes: 19 additions & 2 deletions packages/mobx/src/core/atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,28 @@ import {
propagateChanged,
reportObserved,
startBatch,
Lambda
Lambda,
StrongWeakSet,
queueForUnobservation
} from "../internal"

export const $mobx = Symbol("mobx administration")

export function createObserverStore(observee: IObservable): Set<IDerivation> {
if (typeof WeakRef != "undefined" && typeof FinalizationRegistry != "undefined") {
const store = new StrongWeakSet<IDerivation>(() => {
if (store.size === 0) {
startBatch()
queueForUnobservation(observee)
endBatch()
}
})
return store
} else {
return new Set<IDerivation>()
}
}

export interface IAtom extends IObservable {
reportObserved(): boolean
reportChanged()
Expand All @@ -24,7 +41,7 @@ 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
observers_ = new Set<IDerivation>()
observers_ = createObserverStore(this)

diffValue_ = 0
lastAccessedBy_ = 0
Expand Down
13 changes: 11 additions & 2 deletions packages/mobx/src/core/computedvalue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import {
UPDATE,
die,
allowStateChangesStart,
allowStateChangesEnd
allowStateChangesEnd,
createObserverStore
} from "../internal"

export interface IComputedValue<T> {
Expand All @@ -45,6 +46,12 @@ export interface IComputedValueOptions<T> {
context?: any
requiresReaction?: boolean
keepAlive?: boolean
/**
* Stop any observees from preventing this computed from being garbage collected
*
* This is an advanced feature and primarily intended for use with `keepAlive` computeds.
*/
weak?: boolean
}

export type IComputedDidChange<T = any> = {
Expand Down Expand Up @@ -81,7 +88,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
newObserving_ = null // during tracking it's an array with new observed observers
isBeingObserved_ = false
isPendingUnobservation_: boolean = false
observers_ = new Set<IDerivation>()
observers_ = createObserverStore(this)
diffValue_ = 0
runId_ = 0
lastAccessedBy_ = 0
Expand All @@ -99,6 +106,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
private equals_: IEqualsComparer<any>
private requiresReaction_: boolean | undefined
keepAlive_: boolean
weak_: boolean

/**
* Create a new computed value based on a function expression.
Expand Down Expand Up @@ -132,6 +140,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
this.scope_ = options.context
this.requiresReaction_ = options.requiresReaction
this.keepAlive_ = !!options.keepAlive
this.weak_ = !!options.weak
}

onBecomeStale_() {
Expand Down
2 changes: 2 additions & 0 deletions packages/mobx/src/core/derivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface IDerivation extends IDepTreeNode {
* warn if the derivation has no dependencies after creation/update
*/
requiresObservable_?: boolean

readonly weak_: boolean
}

export class CaughtException {
Expand Down
3 changes: 2 additions & 1 deletion packages/mobx/src/core/reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class Reaction implements IDerivation, IReactionPublic {
public name_: string = __DEV__ ? "Reaction@" + getNextId() : "Reaction",
private onInvalidate_: () => void,
private errorHandler_?: (error: any, derivation: IDerivation) => void,
public requiresObservable_?
public requiresObservable_?,
readonly weak_ = false
) {}

onBecomeStale_() {
Expand Down
1 change: 1 addition & 0 deletions packages/mobx/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ but at least in this file we can magically reorder the imports with trial and er
export * from "./utils/global"
export * from "./errors"
export * from "./utils/utils"
export * from "./utils/weakset"
export * from "./api/decorators"
export * from "./core/atom"
export * from "./utils/comparer"
Expand Down
Loading

0 comments on commit 1bf10cd

Please sign in to comment.