-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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 all 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); | ||
|
@@ -25,13 +24,10 @@ export class ReactiveMap extends Map { | |
if (DEV) new Map(value); | ||
|
||
if (value) { | ||
var sources = this.#sources; | ||
|
||
for (var [key, v] of value) { | ||
sources.set(key, source(v)); | ||
super.set(key, v); | ||
} | ||
|
||
this.#size.v = sources.size; | ||
this.#size.v = super.size; | ||
} | ||
} | ||
|
||
|
@@ -41,14 +37,20 @@ 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; | ||
var ret = super.get(key); | ||
if (ret !== undefined) { | ||
s = source(Symbol()); | ||
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); | ||
|
@@ -62,23 +64,29 @@ 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); | ||
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 undefined; | ||
var ret = super.get(key); | ||
if (ret !== undefined) { | ||
s = source(Symbol()); | ||
sources.set(key, s); | ||
} else { | ||
// We should always track the version in case | ||
// the Set ever gets this value in the future. | ||
get(this.#version); | ||
return undefined; | ||
} | ||
} | ||
|
||
return get(s); | ||
get(s); | ||
return super.get(key); | ||
} | ||
|
||
/** | ||
|
@@ -88,72 +96,71 @@ 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)); | ||
sources.delete(key); | ||
set(this.#size, super.size); | ||
set(s, UNINITIALIZED); | ||
this.#increment_version(); | ||
return removed; | ||
} | ||
|
||
return false; | ||
return res; | ||
} | ||
|
||
clear() { | ||
var sources = this.#sources; | ||
|
||
if (sources.size !== 0) { | ||
if (super.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(); | ||
} | ||
|
||
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]() { | ||
return this.entries(); | ||
} | ||
|
||
get size() { | ||
return get(this.#size); | ||
get(this.#size); | ||
return super.size; | ||
} | ||
} |
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.