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 <rich.harris@vercel.com>
pull/12400/head
Simon H 1 year ago committed by GitHub
parent 42a7a0ecd8
commit 587bbe4b5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: only create a maximum of one document event listener per event

@ -1280,7 +1280,12 @@ function serialize_event(node, context) {
} }
const parent = /** @type {import('#compiler').SvelteNode} */ (context.path.at(-1)); const parent = /** @type {import('#compiler').SvelteNode} */ (context.path.at(-1));
if (parent.type === 'SvelteDocument' || parent.type === 'SvelteWindow') { if (
parent.type === 'SvelteDocument' ||
parent.type === 'SvelteWindow' ||
parent.type === 'SvelteBody'
) {
// These nodes are above the component tree, and its events should run parent first
state.before_init.push(statement); state.before_init.push(statement);
} else { } else {
state.after_update.push(statement); state.after_update.push(statement);

@ -1,9 +1,14 @@
import { teardown } from '../../reactivity/effects.js'; import { teardown } from '../../reactivity/effects.js';
import { all_registered_events, root_event_handles } from '../../render.js';
import { define_property, is_array } from '../../utils.js'; import { define_property, is_array } from '../../utils.js';
import { hydrating } from '../hydration.js'; import { hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js'; import { queue_micro_task } from '../task.js';
/** @type {Set<string>} */
export const all_registered_events = new Set();
/** @type {Set<(events: Array<string>) => void>} */
export const root_event_handles = new Set();
/** /**
* SSR adds onload and onerror attributes to catch those events before the hydration. * SSR adds onload and onerror attributes to catch those events before the hydration.
* This function detects those cases, removes the attributes and replays the events. * This function detects those cases, removes the attributes and replays the events.

@ -1,8 +1,7 @@
/** @import { Effect, TemplateNode } from '#client' */ /** @import { TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js'; import { init_array_prototype_warnings } from '../dev/equality.js';
import { current_effect } from '../runtime.js';
// export these for reference in the compiled code, making global name deduplication unnecessary // export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */ /** @type {Window} */

@ -16,7 +16,11 @@ import {
set_hydrating set_hydrating
} from './dom/hydration.js'; } from './dom/hydration.js';
import { array_from } from './utils.js'; import { array_from } from './utils.js';
import { handle_event_propagation } from './dom/elements/events.js'; import {
all_registered_events,
handle_event_propagation,
root_event_handles
} from './dom/elements/events.js';
import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import { reset_head_anchor } from './dom/blocks/svelte-head.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';
@ -24,12 +28,6 @@ import { validate_component } from '../shared/validate.js';
import { assign_nodes } from './dom/template.js'; import { assign_nodes } from './dom/template.js';
import { queue_micro_task } from './dom/task.js'; import { queue_micro_task } from './dom/task.js';
/** @type {Set<string>} */
export const all_registered_events = new Set();
/** @type {Set<(events: Array<string>) => void>} */
export const root_event_handles = new Set();
/** /**
* This is normally true block effects should run their intro transitions * This is normally true block effects should run their intro transitions
* but is false during hydration (unless `options.intro` is `true`) and * but is false during hydration (unless `options.intro` is `true`) and
@ -182,6 +180,9 @@ export function hydrate(component, options) {
} }
} }
/** @type {Map<string, number>} */
const document_listeners = new Map();
/** /**
* @template {Record<string, any>} Exports * @template {Record<string, any>} Exports
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<any>> | import('../../index.js').Component<any>} Component * @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) {
function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) { function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
init_operations(); init_operations();
const registered_events = new Set(); var registered_events = new Set();
/** @param {Array<string>} events */ /** @param {Array<string>} events */
const event_handle = (events) => { var event_handle = (events) => {
for (let i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
const event_name = events[i]; var event_name = events[i];
const passive = PassiveDelegatedEvents.includes(event_name);
if (!registered_events.has(event_name)) { if (registered_events.has(event_name)) continue;
registered_events.add(event_name); registered_events.add(event_name);
// Add the event listener to both the container and the document. var passive = PassiveDelegatedEvents.includes(event_name);
// The container listener ensures we catch events from within in case
// the outer content stops propagation of the event.
target.addEventListener(event_name, handle_event_propagation, { passive });
// Add the event listener to both the container and the document.
// The container listener ensures we catch events from within in case
// the outer content stops propagation of the event.
target.addEventListener(event_name, handle_event_propagation, { passive });
var n = document_listeners.get(event_name);
if (n === undefined) {
// The document listener ensures we catch events that originate from elements that were // The document listener ensures we catch events that originate from elements that were
// manually moved outside of the container (e.g. via manual portals). // manually moved outside of the container (e.g. via manual portals).
document.addEventListener(event_name, handle_event_propagation, { passive }); document.addEventListener(event_name, handle_event_propagation, { passive });
document_listeners.set(event_name, 1);
} else {
document_listeners.set(event_name, n + 1);
} }
} }
}; };
@ -226,9 +234,9 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
/** @type {Exports} */ /** @type {Exports} */
// @ts-expect-error will be defined because the render effect runs synchronously // @ts-expect-error will be defined because the render effect runs synchronously
let component = undefined; var component = undefined;
const unmount = effect_root(() => { var unmount = effect_root(() => {
branch(() => { branch(() => {
if (context) { if (context) {
push({}); push({});
@ -262,9 +270,17 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
}); });
return () => { return () => {
for (const event_name of registered_events) { for (var event_name of registered_events) {
target.removeEventListener(event_name, handle_event_propagation); target.removeEventListener(event_name, handle_event_propagation);
document.removeEventListener(event_name, handle_event_propagation);
var n = /** @type {number} */ (document_listeners.get(event_name));
if (--n === 0) {
document.removeEventListener(event_name, handle_event_propagation);
document_listeners.delete(event_name);
} else {
document_listeners.set(event_name, n);
}
} }
root_event_handles.delete(event_handle); root_event_handles.delete(event_handle);

Loading…
Cancel
Save