Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curly-ladybugs-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"mobx": minor
---

Add a configure option to disable descriptor cache reuse for observable object keys.
12 changes: 12 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,18 @@ Note it doesn't affect existing observables, only the ones created after it's be
configure({ safeDescriptors: false })
```

#### `useDescriptorCache: boolean`

MobX caches property descriptors for observable plain-object keys so repeated keys can reuse the same getter and setter functions. This is usually a small performance optimization, but the cache can grow without bound when observables are created with a very large number of unique property names, such as UUIDs used as object keys during SSR.

`configure({ useDescriptorCache: false })` disables descriptor reuse and clears any descriptors that were cached so far. Existing observable objects keep working; only future observable properties stop using the cache. **Default: `true`**

```javascript
configure({ useDescriptorCache: false })
```

Whenever possible, prefer `observable.map` for truly dynamic key-based collections.

## Further configuration options

#### `isolateGlobalState: boolean`
Expand Down
38 changes: 38 additions & 0 deletions packages/mobx/__tests__/v5/base/observables.js
Original file line number Diff line number Diff line change
Expand Up @@ -2193,6 +2193,44 @@ test("configure({ safeDescriptors: false })", () => {
expect(globalState.safeDescriptors).toBe(true)
})

test("configure({ useDescriptorCache: false }) disables and evicts cached object descriptors", () => {
const globalState = mobx._getGlobalState()
const getKeyGetter = () =>
Object.getOwnPropertyDescriptor(
mobx.observable.object(
{
key: 1
},
{},
{ proxy: false }
),
"key"
).get

try {
expect(globalState.useDescriptorCache).toBe(true)

const cachedGetter = getKeyGetter()
expect(getKeyGetter()).toBe(cachedGetter)

mobx.configure({ useDescriptorCache: false })
expect(globalState.useDescriptorCache).toBe(false)

const uncachedGetter = getKeyGetter()
expect(uncachedGetter).not.toBe(cachedGetter)
expect(getKeyGetter()).not.toBe(uncachedGetter)

mobx.configure({ useDescriptorCache: true })
expect(globalState.useDescriptorCache).toBe(true)

const recachedGetter = getKeyGetter()
expect(recachedGetter).not.toBe(cachedGetter)
expect(getKeyGetter()).toBe(recachedGetter)
} finally {
mobx.configure({ useDescriptorCache: true })
}
})

test("function props are observable auto actions", () => {
const o = observable({
observable: 0,
Expand Down
14 changes: 13 additions & 1 deletion packages/mobx/src/api/configure.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { globalState, isolateGlobalState, setReactionScheduler } from "../internal"
import {
clearObservablePropDescriptorCache,
globalState,
isolateGlobalState,
setReactionScheduler
} from "../internal"

const NEVER = "never"
const ALWAYS = "always"
Expand All @@ -19,6 +24,7 @@ export function configure(options: {
isolateGlobalState?: boolean
disableErrorBoundaries?: boolean
safeDescriptors?: boolean
useDescriptorCache?: boolean
reactionScheduler?: (f: () => void) => void
useProxies?: "always" | "never" | "ifavailable"
}): void {
Expand All @@ -42,6 +48,12 @@ export function configure(options: {
globalState.enforceActions = ea
globalState.allowStateChanges = ea === true || ea === ALWAYS ? false : true
}
if ("useDescriptorCache" in options) {
globalState.useDescriptorCache = !!options.useDescriptorCache
if (!globalState.useDescriptorCache) {
clearObservablePropDescriptorCache()
}
}
;[
"computedRequiresReaction",
"reactionRequiresObservable",
Expand Down
8 changes: 8 additions & 0 deletions packages/mobx/src/core/globalstate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ export class MobXGlobals {
*/
verifyProxies = false

/*
* Reuse property descriptors for observable plain-object keys.
*/
useDescriptorCache = true

/**
* False forces all object's descriptors to
* writable: true
Expand Down Expand Up @@ -178,6 +183,9 @@ export let globalState: MobXGlobals = (function () {
if (!global.__mobxGlobals.UNCHANGED) {
global.__mobxGlobals.UNCHANGED = {}
} // make merge backward compatible
if (global.__mobxGlobals.useDescriptorCache === undefined) {
global.__mobxGlobals.useDescriptorCache = true
}
return global.__mobxGlobals
} else {
global.__mobxInstanceCount = 1
Expand Down
34 changes: 21 additions & 13 deletions packages/mobx/src/types/observableobject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ import {
checkIfStateModificationsAreAllowed
} from "../internal"

const descriptorCache = Object.create(null)
let descriptorCache = Object.create(null)

export function clearObservablePropDescriptorCache() {
descriptorCache = Object.create(null)
}

export type IObjectDidChange<T = any> = {
observableKind: "object"
Expand Down Expand Up @@ -698,18 +702,22 @@ const isObservableObjectAdministration = createInstanceofPredicate(
ObservableObjectAdministration
)

function getCachedObservablePropDescriptor(key) {
return (
descriptorCache[key] ||
(descriptorCache[key] = {
get() {
return this[$mobx].getObservablePropValue_(key)
},
set(value) {
return this[$mobx].setObservablePropValue_(key, value)
}
})
)
function createObservablePropDescriptor(key: PropertyKey) {
return {
get() {
return this[$mobx].getObservablePropValue_(key)
},
set(value) {
return this[$mobx].setObservablePropValue_(key, value)
}
}
}

function getCachedObservablePropDescriptor(key: PropertyKey) {
if (!globalState.useDescriptorCache) {
return createObservablePropDescriptor(key)
}
return descriptorCache[key] || (descriptorCache[key] = createObservablePropDescriptor(key))
}

export function isObservableObject(thing: any): boolean {
Expand Down