-
-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: improve reactive Map and Set implementations #11827
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"svelte": patch | ||
--- | ||
|
||
fix: improve reactive Map and Set implementations |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,15 +2,14 @@ import { DEV } from 'esm-env'; | |
import { source, set } from '../internal/client/reactivity/sources.js'; | ||
import { get } from '../internal/client/runtime.js'; | ||
import { UNINITIALIZED } from '../constants.js'; | ||
import { map } from './utils.js'; | ||
|
||
/** | ||
* @template K | ||
* @template V | ||
* @extends {Map<K, V>} | ||
*/ | ||
export class ReactiveMap extends Map { | ||
/** @type {Map<K, import('#client').Source<V>>} */ | ||
/** @type {Map<K, import('#client').Source<symbol>>} */ | ||
#sources = new Map(); | ||
#version = source(0); | ||
#size = source(0); | ||
|
@@ -28,7 +27,8 @@ export class ReactiveMap extends Map { | |
var sources = this.#sources; | ||
|
||
for (var [key, v] of value) { | ||
sources.set(key, source(v)); | ||
sources.set(key, source(Symbol())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we check if the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By the way, I'm not sure if we need to fill the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, fixed. |
||
super.set(key, v); | ||
} | ||
|
||
this.#size.v = sources.size; | ||
|
@@ -41,18 +41,24 @@ export class ReactiveMap extends Map { | |
|
||
/** @param {K} key */ | ||
has(key) { | ||
var s = this.#sources.get(key); | ||
var sources = this.#sources; | ||
var s = sources.get(key); | ||
|
||
if (s === undefined) { | ||
// We should always track the version in case | ||
// the Set ever gets this value in the future. | ||
get(this.#version); | ||
|
||
return false; | ||
// If we're working with a non-primitive then we can generate a signal for it | ||
if (typeof key !== 'object' || key === null) { | ||
s = source(UNINITIALIZED); | ||
sources.set(key, s); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still has the same problem as #11504 and #11287. We need a way to remove these signals when we are no longer listening to them. Otherwise it would create a memory leak (see #11504 (comment)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Unfortunately, this means that we can't retain primitives either. So they won't be fine-grain, as there's no way to do so without leaking. |
||
} else { | ||
// We should always track the version in case | ||
// the Set ever gets this value in the future. | ||
get(this.#version); | ||
return false; | ||
} | ||
} | ||
|
||
get(s); | ||
return true; | ||
return super.has(key); | ||
} | ||
|
||
/** | ||
|
@@ -62,23 +68,25 @@ export class ReactiveMap extends Map { | |
forEach(callbackfn, this_arg) { | ||
get(this.#version); | ||
|
||
var bound_callbackfn = callbackfn.bind(this_arg); | ||
this.#sources.forEach((s, key) => bound_callbackfn(s.v, key, this)); | ||
return super.forEach(callbackfn, this_arg); | ||
} | ||
|
||
/** @param {K} key */ | ||
get(key) { | ||
var s = this.#sources.get(key); | ||
|
||
if (s === undefined) { | ||
// We should always track the version in case | ||
// the Set ever gets this value in the future. | ||
get(this.#version); | ||
|
||
return undefined; | ||
// Re-use the has code-path to init the signal if needed and also | ||
// register to the version if needed. | ||
this.has(key); | ||
s = this.#sources.get(key); | ||
if (s === undefined) { | ||
return undefined; | ||
} | ||
} | ||
|
||
return get(s); | ||
get(s); | ||
return super.get(key); | ||
} | ||
|
||
/** | ||
|
@@ -88,32 +96,37 @@ export class ReactiveMap extends Map { | |
set(key, value) { | ||
var sources = this.#sources; | ||
var s = sources.get(key); | ||
var prev_res = super.get(key); | ||
var res = super.set(key, value); | ||
|
||
if (s === undefined) { | ||
sources.set(key, source(value)); | ||
set(this.#size, sources.size); | ||
sources.set(key, source(Symbol())); | ||
set(this.#size, super.size); | ||
this.#increment_version(); | ||
} else { | ||
set(s, value); | ||
} else if (prev_res !== value) { | ||
set(s, Symbol()); | ||
} | ||
|
||
return this; | ||
return res; | ||
} | ||
|
||
/** @param {K} key */ | ||
delete(key) { | ||
var sources = this.#sources; | ||
var s = sources.get(key); | ||
var res = super.delete(key); | ||
|
||
if (s !== undefined) { | ||
var removed = sources.delete(key); | ||
set(this.#size, sources.size); | ||
set(s, /** @type {V} */ (UNINITIALIZED)); | ||
// Remove non-primitive keys | ||
if (typeof key === 'object' && key !== null) { | ||
sources.delete(key); | ||
} | ||
set(this.#size, super.size); | ||
set(s, UNINITIALIZED); | ||
this.#increment_version(); | ||
return removed; | ||
} | ||
|
||
return false; | ||
return res; | ||
} | ||
|
||
clear() { | ||
|
@@ -122,31 +135,28 @@ export class ReactiveMap extends Map { | |
if (sources.size !== 0) { | ||
set(this.#size, 0); | ||
for (var s of sources.values()) { | ||
set(s, /** @type {V} */ (UNINITIALIZED)); | ||
set(s, UNINITIALIZED); | ||
} | ||
this.#increment_version(); | ||
} | ||
|
||
sources.clear(); | ||
super.clear(); | ||
} | ||
|
||
keys() { | ||
get(this.#version); | ||
return this.#sources.keys(); | ||
return super.keys(); | ||
} | ||
|
||
values() { | ||
get(this.#version); | ||
return map(this.#sources.values(), get, 'Map Iterator'); | ||
return super.values(); | ||
} | ||
|
||
entries() { | ||
get(this.#version); | ||
return map( | ||
this.#sources.entries(), | ||
([key, source]) => /** @type {[K, V]} */ ([key, get(source)]), | ||
'Map Iterator' | ||
); | ||
return super.entries(); | ||
} | ||
|
||
[Symbol.iterator]() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could use a WeakMap here to allow for object keys?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah WeakMaps don't give us anything here really. It means we'd have to duplicate data as we need to store the value on the
super
and in the WeakMap, which means the WeakMap never does anything as the value is strongly held. Also WeakMaps are very slow.