Skip to content

Commit 2382eb0

Browse files
authored
fix: improve reactive Map and Set implementations (#11827)
Closes #11727. This PR aims to tackle issues around our reactive Map/Set implementations. Notably: - We now store the values on the backing Map/Set, allowing for much better introspection in console/dev tools - We no longer store the values inside the source signals, instead we use Symbols and booleans only as markers There's one limitation around `.has(x)` when `x` is not in the Map/Set yet - it's not fine-grained. Making it so could create too much memory pressure when e.g. iterating a big list of items with few of them being in the set (`has` returns `false` most of the time).
1 parent 3c84c21 commit 2382eb0

File tree

6 files changed

+176
-88
lines changed

6 files changed

+176
-88
lines changed

.changeset/eight-jeans-compare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: improve reactive Map and Set implementations

packages/svelte/src/internal/client/dev/inspect.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,6 @@ export function inspect(get_value, inspector = console.log) {
6262
*/
6363
function deep_snapshot(value, visited = new Map()) {
6464
if (typeof value === 'object' && value !== null && !visited.has(value)) {
65-
if (DEV) {
66-
// When dealing with ReactiveMap or ReactiveSet, return normal versions
67-
// so that console.log provides better output versions
68-
if (value instanceof Map && value.constructor !== Map) {
69-
return new Map(value);
70-
}
71-
if (value instanceof Set && value.constructor !== Set) {
72-
return new Set(value);
73-
}
74-
}
7565
const unstated = snapshot(value);
7666

7767
if (unstated !== value) {

packages/svelte/src/reactivity/map.js

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import { DEV } from 'esm-env';
22
import { source, set } from '../internal/client/reactivity/sources.js';
33
import { get } from '../internal/client/runtime.js';
44
import { UNINITIALIZED } from '../constants.js';
5-
import { map } from './utils.js';
65

76
/**
87
* @template K
98
* @template V
109
* @extends {Map<K, V>}
1110
*/
1211
export class ReactiveMap extends Map {
13-
/** @type {Map<K, import('#client').Source<V>>} */
12+
/** @type {Map<K, import('#client').Source<symbol>>} */
1413
#sources = new Map();
1514
#version = source(0);
1615
#size = source(0);
@@ -25,13 +24,10 @@ export class ReactiveMap extends Map {
2524
if (DEV) new Map(value);
2625

2726
if (value) {
28-
var sources = this.#sources;
29-
3027
for (var [key, v] of value) {
31-
sources.set(key, source(v));
28+
super.set(key, v);
3229
}
33-
34-
this.#size.v = sources.size;
30+
this.#size.v = super.size;
3531
}
3632
}
3733

@@ -41,14 +37,20 @@ export class ReactiveMap extends Map {
4137

4238
/** @param {K} key */
4339
has(key) {
44-
var s = this.#sources.get(key);
40+
var sources = this.#sources;
41+
var s = sources.get(key);
4542

4643
if (s === undefined) {
47-
// We should always track the version in case
48-
// the Set ever gets this value in the future.
49-
get(this.#version);
50-
51-
return false;
44+
var ret = super.get(key);
45+
if (ret !== undefined) {
46+
s = source(Symbol());
47+
sources.set(key, s);
48+
} else {
49+
// We should always track the version in case
50+
// the Set ever gets this value in the future.
51+
get(this.#version);
52+
return false;
53+
}
5254
}
5355

5456
get(s);
@@ -62,23 +64,29 @@ export class ReactiveMap extends Map {
6264
forEach(callbackfn, this_arg) {
6365
get(this.#version);
6466

65-
var bound_callbackfn = callbackfn.bind(this_arg);
66-
this.#sources.forEach((s, key) => bound_callbackfn(s.v, key, this));
67+
return super.forEach(callbackfn, this_arg);
6768
}
6869

6970
/** @param {K} key */
7071
get(key) {
71-
var s = this.#sources.get(key);
72+
var sources = this.#sources;
73+
var s = sources.get(key);
7274

7375
if (s === undefined) {
74-
// We should always track the version in case
75-
// the Set ever gets this value in the future.
76-
get(this.#version);
77-
78-
return undefined;
76+
var ret = super.get(key);
77+
if (ret !== undefined) {
78+
s = source(Symbol());
79+
sources.set(key, s);
80+
} else {
81+
// We should always track the version in case
82+
// the Set ever gets this value in the future.
83+
get(this.#version);
84+
return undefined;
85+
}
7986
}
8087

81-
return get(s);
88+
get(s);
89+
return super.get(key);
8290
}
8391

8492
/**
@@ -88,72 +96,71 @@ export class ReactiveMap extends Map {
8896
set(key, value) {
8997
var sources = this.#sources;
9098
var s = sources.get(key);
99+
var prev_res = super.get(key);
100+
var res = super.set(key, value);
91101

92102
if (s === undefined) {
93-
sources.set(key, source(value));
94-
set(this.#size, sources.size);
103+
sources.set(key, source(Symbol()));
104+
set(this.#size, super.size);
95105
this.#increment_version();
96-
} else {
97-
set(s, value);
106+
} else if (prev_res !== value) {
107+
set(s, Symbol());
98108
}
99109

100-
return this;
110+
return res;
101111
}
102112

103113
/** @param {K} key */
104114
delete(key) {
105115
var sources = this.#sources;
106116
var s = sources.get(key);
117+
var res = super.delete(key);
107118

108119
if (s !== undefined) {
109-
var removed = sources.delete(key);
110-
set(this.#size, sources.size);
111-
set(s, /** @type {V} */ (UNINITIALIZED));
120+
sources.delete(key);
121+
set(this.#size, super.size);
122+
set(s, UNINITIALIZED);
112123
this.#increment_version();
113-
return removed;
114124
}
115125

116-
return false;
126+
return res;
117127
}
118128

119129
clear() {
120130
var sources = this.#sources;
121131

122-
if (sources.size !== 0) {
132+
if (super.size !== 0) {
123133
set(this.#size, 0);
124134
for (var s of sources.values()) {
125-
set(s, /** @type {V} */ (UNINITIALIZED));
135+
set(s, UNINITIALIZED);
126136
}
127137
this.#increment_version();
138+
sources.clear();
128139
}
129-
130-
sources.clear();
140+
super.clear();
131141
}
132142

133143
keys() {
134144
get(this.#version);
135-
return this.#sources.keys();
145+
return super.keys();
136146
}
137147

138148
values() {
139149
get(this.#version);
140-
return map(this.#sources.values(), get, 'Map Iterator');
150+
return super.values();
141151
}
142152

143153
entries() {
144154
get(this.#version);
145-
return map(
146-
this.#sources.entries(),
147-
([key, source]) => /** @type {[K, V]} */ ([key, get(source)]),
148-
'Map Iterator'
149-
);
155+
return super.entries();
150156
}
151157

152158
[Symbol.iterator]() {
153159
return this.entries();
154160
}
155161

156162
get size() {
157-
return get(this.#size);
163+
get(this.#size);
164+
return super.size;
158165
}
159166
}

packages/svelte/src/reactivity/map.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,35 @@ test('map handling of undefined values', () => {
183183

184184
cleanup();
185185
});
186+
187+
test('not invoking reactivity when value is not in the map after changes', () => {
188+
const map = new ReactiveMap([[1, 1]]);
189+
190+
const log: any = [];
191+
192+
const cleanup = effect_root(() => {
193+
render_effect(() => {
194+
log.push(map.get(1));
195+
});
196+
197+
render_effect(() => {
198+
log.push(map.get(2));
199+
});
200+
201+
flushSync(() => {
202+
map.delete(1);
203+
});
204+
205+
flushSync(() => {
206+
map.set(1, 1);
207+
});
208+
});
209+
210+
assert.deepEqual(log, [1, undefined, undefined, undefined, 1, undefined]);
211+
212+
cleanup();
213+
});
214+
215+
test('Map.instanceOf', () => {
216+
assert.equal(new ReactiveMap() instanceof Map, true);
217+
});

0 commit comments

Comments
 (0)