1

I have following simple JS code (https://stackblitz.com/edit/web-platform-ueq5aq?file=script.js):

const baton = document.querySelector('button');

baton.addEventListener('mousedown', (e) => {
  console.log('baton');
  baton.addEventListener('click', (e) => {
    console.log('baton click');
  });
});

When I click a button, I get 'baton' and 'baton click' logged to console. Now my question is what exactly happens here? As I understand it, the moment script is executed, handler mousedown is added to even queue. When I actually click button, this handler is run, so it's taken from event queue, added to call stack and it is executed. When it is executed, handler "click" is added to event queue.

How actually event onClick is triggered after onMouseDown? How is that related to event queue? Why onMouseDown handler is run before click event happens? I'm asking because I have a lot more complex code where result is different in different scenarios.

When user navigates to page in SPA which contains similiar script, and then clicks button 'baton' order is:

mousedown event -> handler mousedown -> handler click -> click event

And when user reloads page, so SPA is loaded right on that page, and clicks button 'baton' order is:

mousedown event -> click event -> handler mousedown

I am seeking answer and truth. Any help will be greatly appreciated.

Ps. Unfortunately I'm not able to reproduce this error in example repository - it happens in quite complex web app which production code I can't share here for obvious reasons.

Ps2. Just to clarify, because probably it isn't stated clearly enough: I'm not asking "why mousedown event is triggered before click event", but "why mousedown HANDLER is run before click event". This is NOT obvious, because handlers are not run immediately. In order of handler to be run, it first have to wait to call stack to be empty, so event queue can be processed by JS engine.

Furman
  • 2,017
  • 3
  • 25
  • 43

3 Answers3

0

From MDN Web Docs

An element receives a click event when a pointing device button (such as a mouse's primary mouse button) is both pressed and released while the pointer is located inside the element.

So there is a mouseup event and then the click event.


EDIT after question edit:

"why mousedown HANDLER is run before click event?"

Your already executing mousedown handler registers the click handler so how should the click handler run before it?

All click handlers registered in all previous mousedown handlers will run after the mousedown and mouseup events too.

enter image description here

Juraj
  • 3,490
  • 4
  • 18
  • 25
  • Yes that is quite obvious, but that's not what I was asking for, it doesn't explain example I gave in question – Furman Dec 04 '21 at 17:23
  • after mouseup event there is already a click handler registered. but I guess even if you register click in mouseup, it will work – Juraj Dec 04 '21 at 17:37
  • But mousedown handler has to go through event queue, it doesn't happen immediately. My question is why actually here it happens before `click` event? Why in some situations (as I described) it happens after `click` event? I guess size of event queue can matter here but I'm asking for detailed explanation to understand what actually happens here "behind the scenes". – Furman Dec 04 '21 at 21:45
  • click never happens before mousedown (as I quote in my answer). mousedown and mouseup originate in hardware and click is a 'virtual' mouse event based on a pair of down and up – Juraj Dec 05 '21 at 05:45
  • Yes, that is right, but still it is not what I was asking for (or at least I can't see it) - can you provide detailed, step by step explanation of my example? "I'm asking because I have a lot more complex code where result is different in different scenarios. When user navigates to page in SPA which contains similiar script, order is: mousedown event -> handler mousedown -> handler click -> click event And when user reloads page, so SPA is loaded right on that page, order is: mousedown event -> click event -> handler mousedown" – Furman Dec 05 '21 at 23:32
  • @Juraj I feel your pain cuz you're correct about this.. `mousedown` is a whole separate event than `click` (**as in it(click) gets dispatched a GOOD BIT of milliseconds after(mousedown)**) – The Bomb Squad Dec 10 '21 at 00:43
  • @Furman his answer is obvious because how the event logic works **IS** that obvious.. to say it in why the order.. it's because one's mouse goes *down* before one *clicks*.. it's why you can hold down on something.. then move the mouse away from a button and it won't click – The Bomb Squad Dec 10 '21 at 00:44
  • But I wasn't asking about that - this is obvious. Please read carefully "Why onMouseDown handler is run before click event happens?" - I wasn't asking "why mousedown event is triggered before click event", but "why mousedown HANDLER is run before click event". This is NOT obvious (at least to me, but no one explained that so far), because handlers are not run immediately, they have to wait to call stack is empty, so handlers in event queue are processed. And as I said in question, outcome can be very different depending on situation. – Furman Dec 10 '21 at 08:25
  • @Furman, the problem maybe that you **add** an additional handler at every mousedown so with every mouse down you have more and more handlers for the click event – Juraj Dec 10 '21 at 08:40
  • Yes that's what I suspect, that there is some strange race condition depending on size of event queue - but so far I was unable to confirm that. All I was able to do is to inspect number of mousedown handlers attached to document in case of navigating between pages of SPA (and of course it is greater when loading right on target page). But this is even more strange, because in that case, when loading target page directly it seems that order should be "mousedown event -> mousedown handler -> click event", but it is not... It's very confusing to me, hence my question here with bounty... – Furman Dec 10 '21 at 08:46
  • I also feel that I lack fundamental understanding what exactly happens there "under the hood" and that would help me to understand why it's working way I described. – Furman Dec 10 '21 at 08:47
  • Uh sorry I really appreciate your efforts to help - but your edited answer still isn't exactly what I was asking for. Yes, of course click handler will be run after mousedown handler. But I was asking about click event. Why click EVENT happens after/before mousedown HANDLER is run. In my "production" case click handler is completely irrelevant (because actually mousedown handler is removing click event listener). – Furman Dec 10 '21 at 10:28
  • @Furman, if you remove the click listener in mousedown handler, then the click handler is not executed in Firefox and Chrome in simple case. but this may be a case where the behavior is undefined and can be random – Juraj Dec 10 '21 at 10:44
  • Uh not really, if click event happens before mousedown handler is run, then click handler is added to event queue and will be run (despite the fact, that event listener on element has been removed) - and that's the problem in this case. – Furman Dec 10 '21 at 10:58
  • @Furman, how can click event happen before mousedown. you mean a click from previous mousedown/up where it wasn't remove yet? – Juraj Dec 10 '21 at 12:18
  • No, it happens in flow I described, so from specific element. How it can happen - I'm trying to understand that :) And to be clear: click event happens before mousedown HANDLER is run, but still after mousedown event, so order is: mousedown event -> click event -> mousedown event handler – Furman Dec 10 '21 at 12:26
  • Today I'll try to set up sample repository to recreate this error, but this might be difficult to recreate, because right now it happens in quite complex web app – Furman Dec 10 '21 at 12:27
  • I guess we can agree that it is a bad idea to register or unregister a click event handler in mousedown/up handler – Juraj Dec 10 '21 at 12:28
  • `I wasn't asking "why mousedown event is triggered before click event", but "why mousedown HANDLER is run before click event"`.. the mousedown HANDLER is triggered first because **you cannot lift up your mouse(click) without FIRST putting your mouse down(mousedown)**. The literal **TIME** difference between the 2 events make it so that *naturally* the `mousedown` event will always happen before the `click` event.. if your follow-up question asks the difference between mouseup and click, check [this](https://stackoverflow.com/a/14805233/10697213) – The Bomb Squad Dec 10 '21 at 14:39
  • @TheBombSquad Please read again question and my explanations in this thread carefully, I'm not asking about why mousedown happens before click, but why mousedown HANDLER happens before/after (depending on situation) click EVENT. Sorry I don't know how to write this more explicity. – Furman Dec 12 '21 at 10:58
  • 1
    @Furman my point in the comment right above is saying that the handler will always be triggered first because mousedown events have to happen before click events.. the time it humanly takes to go from mousedown to click is way more than enough for the event queue to be empty meaning that mousedown(and its handlers) will always occur before click(and its handlers)(unless some botting is involved).. if that isn't the answer to your question.. you have definitely failed in asking the correct question ;-; – The Bomb Squad Dec 13 '21 at 11:20
  • But in my question I clearly stated, that sometimes, depending on certain condition in complex SPA, mousedown handler is run AFTER click event... it was in my question since beginning. I'm still trying to recreate this in example repository, without success so far. – Furman Dec 13 '21 at 11:48
0

The browser tracks the element you clicked the moused down on. Then it tracks the element you lifted the mouse button on. If the element you lifted the mouse button on is the same element or a child element of the target element. Then a click event is dispatched to the last element you lifted the mouse on. The event then propagates up the element chain to every parent element unless the event is told to stop propagating.

If you click down on element A and mouse up on element B. Then A gets mouse down event, and B gets mouse up event, but neither get a click event. Same thing if you navigate the browser to another page in between the mouse down and mouse up.

John
  • 5,942
  • 3
  • 42
  • 79
  • In my example I always click on `baton` element and both mousedown and mouseup (and click) happens on same element and always on same page - either it's page B of SPA (loaded directly) or page B loaded by navigating from page A. Yet in those two scenarios outcomes are different. – Furman Dec 06 '21 at 13:11
  • 1
    @Furman you're not explaining your situation well. You're either skipping something or not using the right words. It would help if you could provide a working sample of your issue. The link in your post is down. – John Dec 06 '21 at 17:09
  • Sorry I tried to be as clear as possible, but maybe once again: I have situation when if user loads directly page https://example.com/test and he click button 'baton' order is "mousedown event -> click event -> handler mousedown". When user navigates to page https://example.com/test from https://example.com/other (and note this is SPA, so page is not reloaded) order is different: "mousedown event -> click event -> handler mousedown". Navigation in between mousedown and click is not the case. – Furman Dec 08 '21 at 10:06
  • In case of navigation from '/example' to '/test' there are a lot more handlers attached to document and other elements, that's why I suspect and I'm asking about event queue. I suspect some race condition happens here, but I need to understand details of what's happening here to confirm that. – Furman Dec 08 '21 at 10:12
0

Perhaps we should start by clarifying a few things.

Events in the browser, are modeled more like a "nesting hierarchy", then a queue -- How it works is referred to as Event Bubbling -- [Wikipedia][1]

But, essentially what you are doing, when adding an EventListener, is hooking into one or more points of the DOM, and saying hey, when X Event passes through here, use function Y to handle it, before passing it along up the stack.

Once an EventListener has been "added" it remains active waiting to be given an event. What exactly it does is defined in its handler function.

let myYFunction = function( e ) { ... }

let myXListener = baton.addEventListern('X event', myYFunction );

// at this point, anytime 'X event' happens to baton, myYFunction will 
// be called to handle it...

Now let's take a look at your examples, lets break things down a little,

const baton = document.querySelector('button');

This first line, is simply querying the DOM, to find the first element of type 'button' in the page. Right... This is "where" we want to insert our event handler. We could add them to any element, anywhere in the DOM, we could even hook into the 'body' element if we wanted to.

Ok, then you have this bit,

baton.addEventListener('mousedown', (e) => {
  console.log('baton');
  baton.addEventListener('click', (e) => {
    console.log('baton click');
  });
});

Which is "nesting" the creation of the 'click' Event Listener, but only after a 'mousedown' event has been "handled". There is no real reason the 'click' event had to be registered within the function body of the mousedown handler.

If we re-write it a bit, it may be clearer what is actually going on.

baton.addEventListener('mousedown', (e) => {
  console.log('baton mousedown');
}

baton.addEventListener('click', (e) => {
    console.log('baton click');
});

Additionally I would also point out, that how it is being done currently "works" -- but it is actually hiding a tiny bit of sloppy coding... you see every time the 'mousedown' event is triggered a new 'click' eventListener is being registered... so eventually you may end up with many, many, many click handlers responding to a single 'click' event... Check out MDN to learn more about [this][2]


I hope this answers your initial questions as to what is going on.


To your question "When I click a button, I get 'baton' and 'baton click' logged to console. Now my question is what exactly happens here?" -- To me, it would look something like this:

  1. a 'mousedown' eventListener is added, however nothing "executes"
  2. a 'mousedown' event takes place, now your 'mousedown' listener executes its function, which in turn logs out to the console, and registers a new 'click' handler -- but again, does not execute.

Moving forward, steps 1 and 2 are repeated for every 'mousedown' seen by baton. Additionally, for any 'click' event passed through baton --- which happens after every 'mousedown' on baton:

  1. A 'click' event occurs, your 'click' handler is then executed and logs out to the console.

SPA event handling strategies

When working with SPAs, where multiple "pages" are displayed, in the same page load... it can get messy, all these event listeners hanging around piling up on one another. If you are going to employ eventListeners between "Pages" of your SPA, you might want to look into how to "remove" them too. - [MDN][3]

That way, you only have eventListeners active for the current "Page" of your SPA.

Also, consider "generalizing" your handlers, and attaching them higher up in the DOM... This would allow you to have only a few event listeners which "route" events to their "logical" handlers.


Random/Different Behaviors

With the steps outlines above, 1, 2 and 3 and how they don't all happen at the same time. You will see what appears to be random output to the console... try and run something like this, to get a proper sense of things:

let cCount = 0;
let mCount = 0;
let tCount = 0;

const baton = document.querySelector('button');

baton.addEventListener('mousedown', (e) => {
  console.log('mousedown # ' + (mCount++) + ' order:' + tCount++);
  baton.addEventListener('click', (e) => {
    console.log('click # ' + (cCount++) + ' order:' + tCount++);
  });
});

[1]: https://en.wikipedia.org/wiki/Event_bubbling#:~:text=Event%20bubbling%20is%20a%20type,Provided%20the%20handler%20is%20initialized). [2]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener [3]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener

rexfordkelly
  • 1,623
  • 10
  • 15
  • This is quality and detailed answer, thank you for your effort, but I can't agree with some points. About part one: baton.addEventListener('mousedown', (e) => { console.log('baton'); baton.addEventListener('click', (e) => { console.log('baton click'); }); }); is definitely not equal to attaching two handlers separately one after another (as you wrote "If we re-write it a bit, it may be clearer what is actually going on." In my example I wrote click handler is attached conditionally after mousedown handler is run. If in mousedown handler we detach click event listener – Furman Dec 12 '21 at 11:00
  • it will never take place. And this is the case in my "production" code. About part "a 'mousedown' event takes place, now your 'mousedown' listener executes its function". I also can't agree here. When mousedown event takes place, mousedown listener is not executed immediately. It will be executed as soon as call stack is empty, and previous handles from event queue has been executed. – Furman Dec 12 '21 at 11:04
  • About your part "handling SPA" - yes I know about removing unused event listeners and memory leaks, I'm all aware of that. But in real scenario this isn't that simple. This is complex application when some listeners can't be detached because they come from external libraries (sentry for example). – Furman Dec 12 '21 at 11:07
  • Overall thank you for good post, but I'm afraid it doesn't explain my question. I need to understand "why mousedown HANDLER is run before/after (depending on situation) click event". How click event triggered relates to handlers waiting to be run in event queue. I wasn't able to find explanation anywhere, and answers in this question (despite I have no doubts authors gave their best to provide best and detailed explanation) are not explaining it either. – Furman Dec 12 '21 at 11:08
  • As for why sometime before and sometimes after, those are non blocking async functions, if you need to guarantee their execution order, you will need to employ additional state and house keeping strategies. – rexfordkelly Dec 19 '21 at 02:26