Skip to content

Commit 59d94d4

Browse files
committed
add support for WeakRefs
1 parent faf075c commit 59d94d4

File tree

12 files changed

+310
-11
lines changed

12 files changed

+310
-11
lines changed

docs/computeds.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,7 @@ It is recommended to set this one to `true` on very expensive computed values. I
236236
### `keepAlive`
237237

238238
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).
239+
240+
### `weak`
241+
242+
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)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import {
2+
IObservableValue,
3+
autorun,
4+
computed,
5+
observable,
6+
onBecomeObserved,
7+
onBecomeUnobserved,
8+
runInAction
9+
} from "../../../src/mobx"
10+
const gc = require("expose-gc/function")
11+
12+
let events: string[] = []
13+
beforeEach(() => {
14+
events = []
15+
})
16+
17+
function nextFrame() {
18+
return new Promise(accept => setTimeout(accept, 1))
19+
}
20+
21+
async function gc_cycle() {
22+
await nextFrame()
23+
gc()
24+
await nextFrame()
25+
}
26+
27+
test("observables should not hold a reference to weak reactions", async () => {
28+
let x = 0
29+
const o = observable.box(10)
30+
31+
;(() => {
32+
const au = autorun(
33+
() => {
34+
x += o.get()
35+
},
36+
{ weak: true }
37+
)
38+
39+
o.set(5)
40+
expect(x).toEqual(15)
41+
})()
42+
43+
await gc_cycle()
44+
expect((o as any).observers_.size).toEqual(0)
45+
46+
o.set(20)
47+
expect(x).toEqual(15)
48+
})
49+
50+
test("observables should hold a reference to reactions", async () => {
51+
let x = 0
52+
const o = observable.box(10)
53+
;(() => {
54+
autorun(() => {
55+
x += o.get()
56+
}, {})
57+
58+
o.set(5)
59+
})()
60+
61+
await gc_cycle()
62+
expect((o as any).observers_.size).toEqual(1)
63+
64+
o.set(20)
65+
expect(x).toEqual(35)
66+
})
67+
68+
test("observables should not hold a reference to weak computeds", async () => {
69+
const o = observable.box(10)
70+
let wref
71+
;(() => {
72+
const kac = computed(
73+
() => {
74+
return o.get()
75+
},
76+
{ keepAlive: true, weak: true }
77+
)
78+
wref = new WeakRef(kac)
79+
kac.get()
80+
})()
81+
82+
expect(wref.deref()).not.toEqual(null)
83+
await gc_cycle()
84+
expect(wref.deref() == null).toBeTruthy()
85+
expect((o as any).observers_.size).toEqual(0)
86+
})
87+
88+
test("observables should hold a reference to computeds", async () => {
89+
const o = observable.box(10)
90+
let wref
91+
;(() => {
92+
const kac = computed(
93+
() => {
94+
return o.get()
95+
},
96+
{ keepAlive: true }
97+
)
98+
kac.get()
99+
wref = new WeakRef(kac)
100+
})()
101+
102+
expect(wref.deref() != null).toBeTruthy()
103+
await nextFrame()
104+
gc()
105+
await nextFrame()
106+
expect(wref.deref() != null).toBeTruthy()
107+
expect((o as any).observers_.size).toEqual(1)
108+
})
109+
110+
test("garbage collection should trigger onBOU", async () => {
111+
const o = observable.box(10)
112+
113+
onBecomeObserved(o, () => events.push(`o observed`))
114+
onBecomeUnobserved(o, () => events.push(`o unobserved`))
115+
116+
;(() => {
117+
autorun(
118+
() => {
119+
o.get()
120+
},
121+
{ weak: true }
122+
)
123+
})()
124+
125+
expect(events).toEqual(["o observed"])
126+
await gc_cycle()
127+
expect(events).toEqual(["o observed", "o unobserved"])
128+
})

packages/mobx/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"@babel/preset-typescript": "^7.9.0",
4545
"@babel/runtime": "^7.9.2",
4646
"conditional-type-checks": "^1.0.5",
47-
"flow-bin": "^0.123.0"
47+
"flow-bin": "^0.123.0",
48+
"expose-gc": "^1.0.0"
4849
},
4950
"keywords": [
5051
"mobx",

packages/mobx/src/api/autorun.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ export interface IAutorunOptions {
2525
requiresObservable?: boolean
2626
scheduler?: (callback: () => void) => any
2727
onError?: (error: any) => void
28+
/**
29+
* Observees will not prevent this Reaction from being garbage collected and disposed - you'll need to keep a reference to it somewhere
30+
*
31+
* This is an advanced feature that, in 99.99% of cases you won't need.
32+
*/
33+
weak?: boolean
2834
}
2935

3036
/**
@@ -59,7 +65,8 @@ export function autorun(
5965
this.track(reactionRunner)
6066
},
6167
opts.onError,
62-
opts.requiresObservable
68+
opts.requiresObservable,
69+
opts.weak
6370
)
6471
} else {
6572
const scheduler = createSchedulerFromOptions(opts)
@@ -80,7 +87,8 @@ export function autorun(
8087
}
8188
},
8289
opts.onError,
83-
opts.requiresObservable
90+
opts.requiresObservable,
91+
opts.weak
8492
)
8593
}
8694

@@ -152,7 +160,8 @@ export function reaction<T, FireImmediately extends boolean = false>(
152160
}
153161
},
154162
opts.onError,
155-
opts.requiresObservable
163+
opts.requiresObservable,
164+
opts.weak
156165
)
157166

158167
function reactionRunner() {

packages/mobx/src/core/atom.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,32 @@ import {
1111
propagateChanged,
1212
reportObserved,
1313
startBatch,
14-
Lambda
14+
Lambda,
15+
StrongWeakSet,
16+
queueForUnobservation
1517
} from "../internal"
1618

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

21+
export function createObserverStore(observee: IObservable): Set<IDerivation> {
22+
if (
23+
typeof WeakRef != "undefined" &&
24+
typeof FinalizationRegistry != "undefined" &&
25+
typeof Symbol != "undefined"
26+
) {
27+
const store = new StrongWeakSet<IDerivation>(() => {
28+
if (store.size === 0) {
29+
startBatch()
30+
queueForUnobservation(observee)
31+
endBatch()
32+
}
33+
})
34+
return store
35+
} else {
36+
return new Set<IDerivation>()
37+
}
38+
}
39+
1940
export interface IAtom extends IObservable {
2041
reportObserved(): boolean
2142
reportChanged()
@@ -24,7 +45,7 @@ export interface IAtom extends IObservable {
2445
export class Atom implements IAtom {
2546
isPendingUnobservation_ = false // for effective unobserving. BaseAtom has true, for extra optimization, so its onBecomeUnobserved never gets called, because it's not needed
2647
isBeingObserved_ = false
27-
observers_ = new Set<IDerivation>()
48+
observers_ = createObserverStore(this)
2849

2950
diffValue_ = 0
3051
lastAccessedBy_ = 0

packages/mobx/src/core/computedvalue.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
UPDATE,
3030
die,
3131
allowStateChangesStart,
32-
allowStateChangesEnd
32+
allowStateChangesEnd,
33+
createObserverStore
3334
} from "../internal"
3435

3536
export interface IComputedValue<T> {
@@ -45,6 +46,12 @@ export interface IComputedValueOptions<T> {
4546
context?: any
4647
requiresReaction?: boolean
4748
keepAlive?: boolean
49+
/**
50+
* Stop any observees from preventing this computed from being garbage collected
51+
*
52+
* This is an advanced feature and primarily intended for use with `keepAlive` computeds.
53+
*/
54+
weak?: boolean
4855
}
4956

5057
export type IComputedDidChange<T = any> = {
@@ -81,7 +88,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
8188
newObserving_ = null // during tracking it's an array with new observed observers
8289
isBeingObserved_ = false
8390
isPendingUnobservation_: boolean = false
84-
observers_ = new Set<IDerivation>()
91+
observers_ = createObserverStore(this)
8592
diffValue_ = 0
8693
runId_ = 0
8794
lastAccessedBy_ = 0
@@ -99,6 +106,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
99106
private equals_: IEqualsComparer<any>
100107
private requiresReaction_: boolean | undefined
101108
keepAlive_: boolean
109+
weak_: boolean
102110

103111
/**
104112
* Create a new computed value based on a function expression.
@@ -132,6 +140,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
132140
this.scope_ = options.context
133141
this.requiresReaction_ = options.requiresReaction
134142
this.keepAlive_ = !!options.keepAlive
143+
this.weak_ = !!options.weak
135144
}
136145

137146
onBecomeStale_() {

packages/mobx/src/core/derivation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export interface IDerivation extends IDepTreeNode {
5858
* warn if the derivation has no dependencies after creation/update
5959
*/
6060
requiresObservable_?: boolean
61+
62+
readonly weak_: boolean
6163
}
6264

6365
export class CaughtException {

packages/mobx/src/core/reaction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ export class Reaction implements IDerivation, IReactionPublic {
6767
public name_: string = __DEV__ ? "Reaction@" + getNextId() : "Reaction",
6868
private onInvalidate_: () => void,
6969
private errorHandler_?: (error: any, derivation: IDerivation) => void,
70-
public requiresObservable_?
70+
public requiresObservable_?,
71+
readonly weak_ = false
7172
) {}
7273

7374
onBecomeStale_() {

packages/mobx/src/internal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ but at least in this file we can magically reorder the imports with trial and er
88
export * from "./utils/global"
99
export * from "./errors"
1010
export * from "./utils/utils"
11+
export * from "./utils/weakset"
1112
export * from "./api/decorators"
1213
export * from "./core/atom"
1314
export * from "./utils/comparer"

0 commit comments

Comments
 (0)