Skip to content

Commit 63456f1

Browse files
authored
fix: remove memory leak from bind:this (#11194)
* fix: remove memory leak from bind:this * alternative approach * add error * tidy * tidy * add TODO * add TODO * alternative approach
1 parent 9aebae8 commit 63456f1

File tree

5 files changed

+64
-16
lines changed

5 files changed

+64
-16
lines changed

.changeset/itchy-eels-marry.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: remove memory leak from bind:this

packages/svelte/src/internal/client/dom/elements/bindings/this.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { STATE_SYMBOL } from '../../../constants.js';
22
import { effect, render_effect } from '../../../reactivity/effects.js';
33
import { untrack } from '../../../runtime.js';
4+
import { queue_task } from '../../task.js';
45

56
/**
67
* @param {any} bound_value
@@ -47,7 +48,8 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
4748
});
4849

4950
return () => {
50-
effect(() => {
51+
// We cannot use effects in the teardown phase, we we use a microtask instead.
52+
queue_task(() => {
5153
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
5254
update(null, ...parts);
5355
}
Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { run_all } from '../../shared/utils.js';
22

33
let is_task_queued = false;
4-
let is_raf_queued = false;
54

65
/** @type {Array<() => void>} */
76
let current_queued_tasks = [];
8-
/** @type {Array<() => void>} */
9-
let current_raf_tasks = [];
107

118
function process_task() {
129
is_task_queued = false;
@@ -15,11 +12,15 @@ function process_task() {
1512
run_all(tasks);
1613
}
1714

18-
function process_raf_task() {
19-
is_raf_queued = false;
20-
const tasks = current_raf_tasks.slice();
21-
current_raf_tasks = [];
22-
run_all(tasks);
15+
/**
16+
* @param {() => void} fn
17+
*/
18+
export function queue_task(fn) {
19+
if (!is_task_queued) {
20+
is_task_queued = true;
21+
queueMicrotask(process_task);
22+
}
23+
current_queued_tasks.push(fn);
2324
}
2425

2526
/**
@@ -29,7 +30,4 @@ export function flush_tasks() {
2930
if (is_task_queued) {
3031
process_task();
3132
}
32-
if (is_raf_queued) {
33-
process_raf_task();
34-
}
3533
}

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
destroy_effect_children,
88
execute_effect,
99
get,
10+
is_destroying_effect,
1011
is_flushing_effect,
1112
remove_reactions,
1213
schedule_effect,
14+
set_is_destroying_effect,
1315
set_is_flushing_effect,
1416
set_signal_status,
1517
untrack
@@ -109,6 +111,12 @@ export function user_effect(fn) {
109111
(DEV ? ': The Svelte $effect rune can only be used during component initialisation.' : '')
110112
);
111113
}
114+
if (is_destroying_effect) {
115+
throw new Error(
116+
'ERR_SVELTE_EFFECT_IN_TEARDOWN' +
117+
(DEV ? ': The Svelte $effect rune can not be used in the teardown phase of an effect.' : '')
118+
);
119+
}
112120

113121
// Non-nested `$effect(...)` in a component should be deferred
114122
// until the component is mounted
@@ -140,6 +148,14 @@ export function user_pre_effect(fn) {
140148
: '')
141149
);
142150
}
151+
if (is_destroying_effect) {
152+
throw new Error(
153+
'ERR_SVELTE_EFFECT_IN_TEARDOWN' +
154+
(DEV
155+
? ': The Svelte $effect.pre rune can not be used in the teardown phase of an effect.'
156+
: '')
157+
);
158+
}
143159

144160
return render_effect(fn);
145161
}
@@ -228,6 +244,22 @@ export function branch(fn) {
228244
return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true);
229245
}
230246

247+
/**
248+
* @param {import("#client").Effect} effect
249+
*/
250+
export function execute_effect_teardown(effect) {
251+
var teardown = effect.teardown;
252+
if (teardown !== null) {
253+
const previously_destroying_effect = is_destroying_effect;
254+
set_is_destroying_effect(true);
255+
try {
256+
teardown.call(null);
257+
} finally {
258+
set_is_destroying_effect(previously_destroying_effect);
259+
}
260+
}
261+
}
262+
231263
/**
232264
* @param {import('#client').Effect} effect
233265
* @returns {void}
@@ -249,7 +281,7 @@ export function destroy_effect(effect) {
249281
}
250282
}
251283

252-
effect.teardown?.call(null);
284+
execute_effect_teardown(effect);
253285

254286
var parent = effect.parent;
255287

packages/svelte/src/internal/client/runtime.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import {
88
object_prototype
99
} from './utils.js';
1010
import { snapshot } from './proxy.js';
11-
import { destroy_effect, effect, user_pre_effect } from './reactivity/effects.js';
11+
import {
12+
destroy_effect,
13+
effect,
14+
execute_effect_teardown,
15+
user_pre_effect
16+
} from './reactivity/effects.js';
1217
import {
1318
EFFECT,
1419
RENDER_EFFECT,
@@ -37,12 +42,18 @@ let current_scheduler_mode = FLUSH_MICROTASK;
3742
// Used for handling scheduling
3843
let is_micro_task_queued = false;
3944
export let is_flushing_effect = false;
45+
export let is_destroying_effect = false;
4046

4147
/** @param {boolean} value */
4248
export function set_is_flushing_effect(value) {
4349
is_flushing_effect = value;
4450
}
4551

52+
/** @param {boolean} value */
53+
export function set_is_destroying_effect(value) {
54+
is_destroying_effect = value;
55+
}
56+
4657
// Used for $inspect
4758
export let is_batching_effect = false;
4859
let is_inspecting_signal = false;
@@ -406,7 +417,7 @@ export function execute_effect(effect) {
406417
destroy_effect_children(effect);
407418
}
408419

409-
effect.teardown?.call(null);
420+
execute_effect_teardown(effect);
410421
var teardown = execute_reaction_fn(effect);
411422
effect.teardown = typeof teardown === 'function' ? teardown : null;
412423
} finally {
@@ -658,11 +669,11 @@ export function flush_sync(fn, flush_previous = true) {
658669

659670
var result = fn?.();
660671

672+
flush_tasks();
661673
if (current_queued_root_effects.length > 0 || root_effects.length > 0) {
662674
flush_sync();
663675
}
664676

665-
flush_tasks();
666677
flush_count = 0;
667678

668679
return result;

0 commit comments

Comments
 (0)