Open madacol opened 4 months ago
There's not much we can do about this. We can list this as a breaking change, but the event object is shared along the listeners to improve performance. I thought we might be able to detect an async closure and bail-out but you could pass that along anywhere with the event object.
Yeah, I thought this was just a bug of svelte. I didn't realized it was almost standard behaviour (in firefox event.currentTarget
goes to null
in a setTimeout)
@madacol I don't know what happened to on:click
in svelte5, but in svelte4, the behavior also exists.
@trueadm So I don't think there's a "breaking change" there from svelte4.
The question instead is why the on:click
behavior is different in svelte5 and svelte4?
Try the following code in the Svelte4 REPL and Svelte5 REPL:
<button on:click={async event => {
const button = event.currentTarget;
console.log(event.currentTarget === button); // true
await new Promise((resolve) => setTimeout(resolve, 100));
console.log(event.currentTarget === button); // false
}}>Click me</button>
Hey @Conduitry this is actually a bug?, two things I noticed after I went through the code.
(For now we will just compare v4 and v5(which includes v4 behind the hood))
on:Click : I use on:Click
then it's adding a signaling event
i.e.,
$.event(
"click",
button_1,
async (event) => {
debugger;
const button = event.currentTarget;
console.log(event.currentTarget === button); // true
await new Promise((resolve) => setTimeout(resolve, 100));
debugger;
console.log(event.currentTarget === button); // false
},
false
);
which means the currentTarget is button and the param dom
is button and after await
its uses the same event
object with same currentTarget
function event(event_name, dom, handler, capture, passive)
onclick: this handled entirely in a very different way i.e.,
const event_handle = (events) => {
for (let i = 0; i < events.length; i++) {
const event_name = events[i];
if (!registered_events.has(event_name)) {
registered_events.add(event_name);
// 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,
bound_event_listener,
PassiveDelegatedEvents.includes(event_name)
? {
passive: true
}
: undefined
);
// The document listener ensures we catch events that originate from elements that were
// manually moved outside of the container (e.g. via manual portals).
document.addEventListener(
event_name,
bound_document_event_listener,
PassiveDelegatedEvents.includes(event_name)
? {
passive: true
}
: undefined
);
}
}
};
Here two event listeners are added one in the root, which can be body and another is dom, now first listnere got called but in this case the handler_element
is not button its body or root div, and so svelte, internally assign the currentTarget to button,
Till now on the first check i.e., event.currentTarget === button
it will say true but actually is not bubbled more like captured form the root of the app.
Now await is called in between 2nd listener is activated and now the handler element
is dom
means the current object is now
changed, so on the second check it will say false i.e., event.currentTarget === button
Just wanted to understand, is making changes in the event object, will not create more bugs?
@trueadm How relevant is this to performance? I think this might be quite pernicious (and documentation usually will not cut it).
What other frameworks do, and what we could also offer, is a way to opt out of event delegation through a compiler option. I'm not sure that this bug right here warrants changing anything though - especially considering that other frameworks like React and Solid have the same gotcha.
Isn't supporting the v4 methodology is better? and make the on:click:
same as onClick
, I don't understand why rethinking is needed here, is js
size is the issue?
@brunnerh Event delegation has a significant impact on performance in real world applications. React has an event. persist()
that will generate a static event object. We could do something the same but with an imported helper that we document.
For the record, these are the differences I have noticed about how and when event.currentTarget
changes reference:
onclick
await
, .then()
, or inside a setTimeout
(including when the Promise resolves synchronously)<body>
on:click
never changes reference
on:click
and in vanilla JS onclick
await
and .then()
where the Promise resolves asynchronously, also inside a setTimeout
, etc)null
E.g it changes when doing
await new Promise((resolve)=>setTimeout(resolve,1000))
but not on
await new Promise((resolve)=>resolve())
Tested in Firefox, and some in chromium
@madacol Yes, as I mentioned above, this is to be expected when using onclick
in Svelte 5. We're doing our own event delegation for improved memory/performance (like many other frameworks do) and sharing the event object between listeners is an optimization. If you use on:click
we don't do this in Svelte 5 as we opt out of event delegation to keep parity with Svelte 4.
@madacol
when the Promise resolves synchronously ???
No, there is no synchronous resolves async
function.
What you see is that the entire handler is delayed asynchronously.
This is what await
actually does. You can never predict the execution order and timing of async
functions.
What we need to know is that according to the documentation, currentTarget
always points to the element to which the event is currently propagated in the bubbling order.
So yes, when the async
function finished before the event bubbles up to the next element, it behaves like currentTarget
has not changed, which is why I gave it an explicit delay with setTimeout
.
Describe the bug
The first
event.currentTarget
correctly references thebutton
, but after awaiting the promise (or in a then() ), it changes reference to thebody
Problem disappears if I change the
onclick
to the oldon:click
Reproduction
https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAA52Qvw6CMBDGX-XSCRIDO1oS4ws4uKlDLSdpLD3SXjGE8O6WiE5OTvfvu--Xu0ncjcUgqvMknOpQVGLf92IjeOyXIgxoGVMdKHq9dHa3yEwOyGlr9ENOKoxOAw7oGGQN08VdWJMLDKtSvoeFjt6neFK-Rd5-ZGSxsNRmPzQgpVxN8i2UJazsetlVT2UYHD7h6KkzAbPMY3IbMJf1mmV5_h-GmjFB5rk-LDdCh7vyi07P6Kgxd4ONqNhHnK_zC1Gr0X5HAQAA
Logs
No response
System Info
Severity
annoyance