Skip to content

Commit 59ea0b9

Browse files
authored
fix: better event handling (#12722)
* simplify * fix/simplify * fix/simplify * start getting a grip of this mess * tidy up * more * more * more * tidy up * make things a bit less weird * tweak * more * more * add once once * consolidate event handling code * some progress. man, this stuff is entangled * more * tidy up * simplify * simplify * more * fix * fix test names * fix a bug * tidy up * changeset * simplify * regenerate * tidy up * tidy up * tidy up * simplify * the module declaration case is already accounted for, above * simplify/document * typo * "hoistable" is a misnomer * hoist non_hoistable, rename * more typos * tweak * regenerate
1 parent e78cfd3 commit 59ea0b9

File tree

34 files changed

+414
-362
lines changed

34 files changed

+414
-362
lines changed

.changeset/smart-cars-know.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: avoid recreating handlers for component events

.changeset/wild-pumas-count.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: call correct event handler for properties of non-reactive objects

packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export function Attribute(node, context) {
4040
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
4141

4242
if (delegated_event !== null) {
43-
if (delegated_event.type === 'hoistable') {
44-
delegated_event.function.metadata.hoistable = true;
43+
if (delegated_event.hoisted) {
44+
delegated_event.function.metadata.hoisted = true;
4545
}
4646

4747
node.metadata.delegated = delegated_event;
@@ -50,6 +50,9 @@ export function Attribute(node, context) {
5050
}
5151
}
5252

53+
/** @type {DelegatedEvent} */
54+
const unhoisted = { hoisted: false };
55+
5356
/**
5457
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
5558
* @param {string} event_name
@@ -58,26 +61,24 @@ export function Attribute(node, context) {
5861
* @returns {null | DelegatedEvent}
5962
*/
6063
function get_delegated_event(event_name, handler, context) {
61-
// Handle delegated event handlers. Bail-out if not a delegated event.
64+
// Handle delegated event handlers. Bail out if not a delegated event.
6265
if (!handler || !is_delegated(event_name)) {
6366
return null;
6467
}
6568

66-
// If we are not working with a RegularElement, then bail-out.
69+
// If we are not working with a RegularElement, then bail out.
6770
const element = context.path.at(-1);
6871
if (element?.type !== 'RegularElement') {
6972
return null;
7073
}
7174

72-
/** @type {DelegatedEvent} */
73-
const non_hoistable = { type: 'non-hoistable' };
7475
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
7576
let target_function = null;
7677
let binding = null;
7778

7879
if (element.metadata.has_spread) {
7980
// event attribute becomes part of the dynamic spread array
80-
return non_hoistable;
81+
return unhoisted;
8182
}
8283

8384
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
@@ -86,14 +87,14 @@ function get_delegated_event(event_name, handler, context) {
8687
binding = context.state.scope.get(handler.name);
8788

8889
if (context.state.analysis.module.scope.references.has(handler.name)) {
89-
// If a binding with the same name is referenced in the module scope (even if not declared there), bail-out
90-
return non_hoistable;
90+
// If a binding with the same name is referenced in the module scope (even if not declared there), bail out
91+
return unhoisted;
9192
}
9293

9394
if (binding != null) {
9495
for (const { path } of binding.references) {
9596
const parent = path.at(-1);
96-
if (parent == null) return non_hoistable;
97+
if (parent === undefined) return unhoisted;
9798

9899
const grandparent = path.at(-2);
99100

@@ -120,17 +121,17 @@ function get_delegated_event(event_name, handler, context) {
120121
element.metadata.has_spread ||
121122
!is_delegated(event_name)
122123
) {
123-
return non_hoistable;
124+
return unhoisted;
124125
}
125126
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
126-
return non_hoistable;
127+
return unhoisted;
127128
}
128129
}
129130
}
130131

131-
// If the binding is exported, bail-out
132+
// If the binding is exported, bail out
132133
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
133-
return non_hoistable;
134+
return unhoisted;
134135
}
135136

136137
if (binding !== null && binding.initial !== null && !binding.mutated && !binding.is_called) {
@@ -146,27 +147,25 @@ function get_delegated_event(event_name, handler, context) {
146147
}
147148
}
148149

149-
// If we can't find a function, bail-out
150-
if (target_function == null) return non_hoistable;
151-
// If the function is marked as non-hoistable, bail-out
152-
if (target_function.metadata.hoistable === 'impossible') return non_hoistable;
153-
// If the function has more than one arg, then bail-out
154-
if (target_function.params.length > 1) return non_hoistable;
150+
// If we can't find a function, or the function has multiple parameters, bail out
151+
if (target_function == null || target_function.params.length > 1) {
152+
return unhoisted;
153+
}
155154

156155
const visited_references = new Set();
157156
const scope = target_function.metadata.scope;
158157
for (const [reference] of scope.references) {
159-
// Bail-out if the arguments keyword is used
160-
if (reference === 'arguments') return non_hoistable;
161-
// Bail-out if references a store subscription
162-
if (scope.get(`$${reference}`)?.kind === 'store_sub') return non_hoistable;
158+
// Bail out if the arguments keyword is used
159+
if (reference === 'arguments') return unhoisted;
160+
// Bail out if references a store subscription
161+
if (scope.get(`$${reference}`)?.kind === 'store_sub') return unhoisted;
163162

164163
const binding = scope.get(reference);
165164
const local_binding = context.state.scope.get(reference);
166165

167166
// If we are referencing a binding that is shadowed in another scope then bail out.
168167
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
169-
return non_hoistable;
168+
return unhoisted;
170169
}
171170

172171
// If we have multiple references to the same store using $ prefix, bail out.
@@ -175,30 +174,30 @@ function get_delegated_event(event_name, handler, context) {
175174
binding.kind === 'store_sub' &&
176175
visited_references.has(reference.slice(1))
177176
) {
178-
return non_hoistable;
177+
return unhoisted;
179178
}
180179

181-
// If we reference the index within an each block, then bail-out.
182-
if (binding !== null && binding.initial?.type === 'EachBlock') return non_hoistable;
180+
// If we reference the index within an each block, then bail out.
181+
if (binding !== null && binding.initial?.type === 'EachBlock') return unhoisted;
183182

184183
if (
185184
binding !== null &&
186-
// Bail-out if the the binding is a rest param
185+
// Bail out if the the binding is a rest param
187186
(binding.declaration_kind === 'rest_param' ||
188-
// Bail-out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
187+
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
189188
(((!context.state.analysis.runes && binding.kind === 'each') ||
190189
// or any normal not reactive bindings that are mutated.
191190
binding.kind === 'normal' ||
192191
// or any reactive imports (those are rewritten) (can only happen in legacy mode)
193192
binding.kind === 'legacy_reactive_import') &&
194193
binding.mutated))
195194
) {
196-
return non_hoistable;
195+
return unhoisted;
197196
}
198197
visited_references.add(reference);
199198
}
200199

201-
return { type: 'hoistable', function: target_function };
200+
return { hoisted: true, function: target_function };
202201
}
203202

204203
/**

packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
export function visit_function(node, context) {
99
// TODO retire this in favour of a more general solution based on bindings
1010
node.metadata = {
11-
// module context -> already hoisted
12-
hoistable: context.state.ast_type === 'module' ? 'impossible' : false,
13-
hoistable_params: [],
11+
hoisted: false,
12+
hoisted_params: [],
1413
scope: context.state.scope
1514
};
1615

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ export function build_proxy_reassignment(value, proxy_reference) {
481481
* @param {ComponentContext} context
482482
* @returns {Pattern[]}
483483
*/
484-
function get_hoistable_params(node, context) {
484+
function get_hoisted_params(node, context) {
485485
const scope = context.state.scope;
486486

487487
/** @type {Identifier[]} */
@@ -549,15 +549,15 @@ function get_hoistable_params(node, context) {
549549
* @param {ComponentContext} context
550550
* @returns {Pattern[]}
551551
*/
552-
export function build_hoistable_params(node, context) {
553-
const hoistable_params = get_hoistable_params(node, context);
554-
node.metadata.hoistable_params = hoistable_params;
552+
export function build_hoisted_params(node, context) {
553+
const hoisted_params = get_hoisted_params(node, context);
554+
node.metadata.hoisted_params = hoisted_params;
555555

556556
/** @type {Pattern[]} */
557557
const params = [];
558558

559559
if (node.params.length === 0) {
560-
if (hoistable_params.length > 0) {
560+
if (hoisted_params.length > 0) {
561561
// For the event object
562562
params.push(b.id('_'));
563563
}
@@ -567,7 +567,7 @@ export function build_hoistable_params(node, context) {
567567
}
568568
}
569569

570-
params.push(...hoistable_params);
570+
params.push(...hoisted_params);
571571
return params;
572572
}
573573

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/** @import { Attribute } from '#compiler' */
22
/** @import { ComponentContext } from '../types' */
33
import { is_event_attribute } from '../../../../utils/ast.js';
4-
import { build_event_attribute } from './shared/element.js';
4+
import { visit_event_attribute } from './shared/events.js';
55

66
/**
77
* @param {Attribute} node
88
* @param {ComponentContext} context
99
*/
1010
export function Attribute(node, context) {
1111
if (is_event_attribute(node)) {
12-
build_event_attribute(node, context);
12+
visit_event_attribute(node, context);
1313
}
1414
}

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

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
11
/** @import { FunctionDeclaration } from 'estree' */
22
/** @import { ComponentContext } from '../types' */
3-
import { build_hoistable_params } from '../utils.js';
3+
import { build_hoisted_params } from '../utils.js';
44
import * as b from '../../../../utils/builders.js';
55

66
/**
77
* @param {FunctionDeclaration} node
88
* @param {ComponentContext} context
99
*/
1010
export function FunctionDeclaration(node, context) {
11-
const metadata = node.metadata;
12-
1311
const state = { ...context.state, in_constructor: false };
1412

15-
if (metadata?.hoistable === true) {
16-
const params = build_hoistable_params(node, context);
13+
if (node.metadata?.hoisted === true) {
14+
const params = build_hoisted_params(node, context);
15+
const body = context.visit(node.body, state);
1716

18-
context.state.hoisted.push(
19-
/** @type {FunctionDeclaration} */ ({
20-
...node,
21-
id: node.id !== null ? context.visit(node.id, state) : null,
22-
params,
23-
body: context.visit(node.body, state)
24-
})
25-
);
17+
context.state.hoisted.push(/** @type {FunctionDeclaration} */ ({ ...node, params, body }));
2618

2719
return b.empty;
2820
}
Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
1-
/** @import { OnDirective } from '#compiler' */
1+
/** @import { OnDirective, SvelteNode } from '#compiler' */
22
/** @import { ComponentContext } from '../types' */
3-
import { build_event } from './shared/element.js';
3+
import * as b from '../../../../utils/builders.js';
4+
import { build_event, build_event_handler } from './shared/events.js';
5+
6+
const modifiers = [
7+
'stopPropagation',
8+
'stopImmediatePropagation',
9+
'preventDefault',
10+
'self',
11+
'trusted',
12+
'once'
13+
];
414

515
/**
616
* @param {OnDirective} node
717
* @param {ComponentContext} context
818
*/
919
export function OnDirective(node, context) {
10-
build_event(node, node.metadata.expression, context);
20+
if (!node.expression) {
21+
context.state.analysis.needs_props = true;
22+
}
23+
24+
let handler = build_event_handler(node.expression, node.metadata.expression, context);
25+
26+
for (const modifier of modifiers) {
27+
if (node.modifiers.includes(modifier)) {
28+
handler = b.call('$.' + modifier, handler);
29+
}
30+
}
31+
32+
const capture = node.modifiers.includes('capture');
33+
const passive =
34+
node.modifiers.includes('passive') ||
35+
(node.modifiers.includes('nonpassive') ? false : undefined);
36+
37+
return build_event(node.name, context.state.node, handler, capture, passive);
1138
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import {
2424
get_attribute_name,
2525
build_attribute_value,
2626
build_class_directives,
27-
build_event_attribute,
2827
build_style_directives
2928
} from './shared/element.js';
3029
import { process_children } from './shared/fragment.js';
3130
import { build_render_statement, build_update, build_update_assignment } from './shared/utils.js';
31+
import { visit_event_attribute } from './shared/events.js';
3232

3333
/**
3434
* @param {RegularElement} node
@@ -138,6 +138,13 @@ export function RegularElement(node, context) {
138138
style_directives.push(attribute);
139139
} else if (attribute.type === 'LetDirective') {
140140
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
141+
} else if (attribute.type === 'OnDirective') {
142+
const handler = /** @type {Expression} */ (context.visit(attribute));
143+
const has_action_directive = node.attributes.find((a) => a.type === 'UseDirective');
144+
145+
context.state.after_update.push(
146+
b.stmt(has_action_directive ? b.call('$.effect', b.thunk(handler)) : handler)
147+
);
141148
} else {
142149
if (attribute.type === 'BindDirective') {
143150
if (attribute.name === 'group' || attribute.name === 'checked') {
@@ -214,7 +221,7 @@ export function RegularElement(node, context) {
214221
) {
215222
might_need_event_replaying = true;
216223
}
217-
build_event_attribute(attribute, context);
224+
visit_event_attribute(attribute, context);
218225
continue;
219226
}
220227

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
/** @import { SvelteBody } from '#compiler' */
22
/** @import { ComponentContext } from '../types' */
3-
import * as b from '../../../../utils/builders.js';
3+
import { visit_special_element } from './shared/special_element.js';
44

55
/**
66
* @param {SvelteBody} node
77
* @param {ComponentContext} context
88
*/
99
export function SvelteBody(node, context) {
10-
context.next({
11-
...context.state,
12-
node: b.id('$.document.body')
13-
});
10+
visit_special_element(node, '$.document.body', context);
1411
}
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
/** @import { SvelteDocument } from '#compiler' */
22
/** @import { ComponentContext } from '../types' */
3-
import * as b from '../../../../utils/builders.js';
3+
import { visit_special_element } from './shared/special_element.js';
44

55
/**
66
* @param {SvelteDocument} node
77
* @param {ComponentContext} context
88
*/
99
export function SvelteDocument(node, context) {
10-
context.next({
11-
...context.state,
12-
node: b.id('$.document')
13-
});
10+
visit_special_element(node, '$.document', context);
1411
}

0 commit comments

Comments
 (0)