Skip to content

Commit 587bbe4

Browse files
fix: more event handling tweaks (#12383)
* fix: more event handling tweaks - ensure we only have a single document listener per event+runtime - add `<svelte:body>` listeners to `before_init` similar to the document/window elements - move some code into `events.js` where it belongs * add a counter * changeset --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 42a7a0e commit 587bbe4

File tree

5 files changed

+56
-26
lines changed

5 files changed

+56
-26
lines changed

.changeset/great-plums-pretend.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+
feat: only create a maximum of one document event listener per event

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1280,7 +1280,12 @@ function serialize_event(node, context) {
12801280
}
12811281

12821282
const parent = /** @type {import('#compiler').SvelteNode} */ (context.path.at(-1));
1283-
if (parent.type === 'SvelteDocument' || parent.type === 'SvelteWindow') {
1283+
if (
1284+
parent.type === 'SvelteDocument' ||
1285+
parent.type === 'SvelteWindow' ||
1286+
parent.type === 'SvelteBody'
1287+
) {
1288+
// These nodes are above the component tree, and its events should run parent first
12841289
state.before_init.push(statement);
12851290
} else {
12861291
state.after_update.push(statement);

packages/svelte/src/internal/client/dom/elements/events.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { teardown } from '../../reactivity/effects.js';
2-
import { all_registered_events, root_event_handles } from '../../render.js';
32
import { define_property, is_array } from '../../utils.js';
43
import { hydrating } from '../hydration.js';
54
import { queue_micro_task } from '../task.js';
65

6+
/** @type {Set<string>} */
7+
export const all_registered_events = new Set();
8+
9+
/** @type {Set<(events: Array<string>) => void>} */
10+
export const root_event_handles = new Set();
11+
712
/**
813
* SSR adds onload and onerror attributes to catch those events before the hydration.
914
* This function detects those cases, removes the attributes and replays the events.

packages/svelte/src/internal/client/dom/operations.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
/** @import { Effect, TemplateNode } from '#client' */
1+
/** @import { TemplateNode } from '#client' */
22
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
33
import { DEV } from 'esm-env';
44
import { init_array_prototype_warnings } from '../dev/equality.js';
5-
import { current_effect } from '../runtime.js';
65

76
// export these for reference in the compiled code, making global name deduplication unnecessary
87
/** @type {Window} */

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

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,18 @@ import {
1616
set_hydrating
1717
} from './dom/hydration.js';
1818
import { array_from } from './utils.js';
19-
import { handle_event_propagation } from './dom/elements/events.js';
19+
import {
20+
all_registered_events,
21+
handle_event_propagation,
22+
root_event_handles
23+
} from './dom/elements/events.js';
2024
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
2125
import * as w from './warnings.js';
2226
import * as e from './errors.js';
2327
import { validate_component } from '../shared/validate.js';
2428
import { assign_nodes } from './dom/template.js';
2529
import { queue_micro_task } from './dom/task.js';
2630

27-
/** @type {Set<string>} */
28-
export const all_registered_events = new Set();
29-
30-
/** @type {Set<(events: Array<string>) => void>} */
31-
export const root_event_handles = new Set();
32-
3331
/**
3432
* This is normally true — block effects should run their intro transitions —
3533
* but is false during hydration (unless `options.intro` is `true`) and
@@ -182,6 +180,9 @@ export function hydrate(component, options) {
182180
}
183181
}
184182

183+
/** @type {Map<string, number>} */
184+
const document_listeners = new Map();
185+
185186
/**
186187
* @template {Record<string, any>} Exports
187188
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<any>> | import('../../index.js').Component<any>} Component
@@ -198,25 +199,32 @@ export function hydrate(component, options) {
198199
function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
199200
init_operations();
200201

201-
const registered_events = new Set();
202+
var registered_events = new Set();
202203

203204
/** @param {Array<string>} events */
204-
const event_handle = (events) => {
205-
for (let i = 0; i < events.length; i++) {
206-
const event_name = events[i];
207-
const passive = PassiveDelegatedEvents.includes(event_name);
205+
var event_handle = (events) => {
206+
for (var i = 0; i < events.length; i++) {
207+
var event_name = events[i];
208208

209-
if (!registered_events.has(event_name)) {
210-
registered_events.add(event_name);
209+
if (registered_events.has(event_name)) continue;
210+
registered_events.add(event_name);
211211

212-
// Add the event listener to both the container and the document.
213-
// The container listener ensures we catch events from within in case
214-
// the outer content stops propagation of the event.
215-
target.addEventListener(event_name, handle_event_propagation, { passive });
212+
var passive = PassiveDelegatedEvents.includes(event_name);
216213

214+
// Add the event listener to both the container and the document.
215+
// The container listener ensures we catch events from within in case
216+
// the outer content stops propagation of the event.
217+
target.addEventListener(event_name, handle_event_propagation, { passive });
218+
219+
var n = document_listeners.get(event_name);
220+
221+
if (n === undefined) {
217222
// The document listener ensures we catch events that originate from elements that were
218223
// manually moved outside of the container (e.g. via manual portals).
219224
document.addEventListener(event_name, handle_event_propagation, { passive });
225+
document_listeners.set(event_name, 1);
226+
} else {
227+
document_listeners.set(event_name, n + 1);
220228
}
221229
}
222230
};
@@ -226,9 +234,9 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
226234

227235
/** @type {Exports} */
228236
// @ts-expect-error will be defined because the render effect runs synchronously
229-
let component = undefined;
237+
var component = undefined;
230238

231-
const unmount = effect_root(() => {
239+
var unmount = effect_root(() => {
232240
branch(() => {
233241
if (context) {
234242
push({});
@@ -262,9 +270,17 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
262270
});
263271

264272
return () => {
265-
for (const event_name of registered_events) {
273+
for (var event_name of registered_events) {
266274
target.removeEventListener(event_name, handle_event_propagation);
267-
document.removeEventListener(event_name, handle_event_propagation);
275+
276+
var n = /** @type {number} */ (document_listeners.get(event_name));
277+
278+
if (--n === 0) {
279+
document.removeEventListener(event_name, handle_event_propagation);
280+
document_listeners.delete(event_name);
281+
} else {
282+
document_listeners.set(event_name, n);
283+
}
268284
}
269285

270286
root_event_handles.delete(event_handle);

0 commit comments

Comments
 (0)