Callback function executing in the call stack even when it's not empty
Asked Answered
K

2

6

When the click event is fired from the mouse, it behaves as expected:

First the listener 1 is pushed into the stack where it queues promise 1 in Microtask Queue(or Job Queue). When listener 1 is popped off, the stack becomes empty. And the promise 1 callback is executed before the listener 2(which is waiting in the Task Queue(or Callback Queue). After promise 1 callback is popped off, the listener 2 is pushed into the stack. So the output is :

Listener 1 Microtask 1 Listener 2 Microtask 2

However when the click is triggered via JavaScript code, it behaves differently:

The callback is pushed into the stack even before the click() function is completed (i.e. call stack is not empty). The output here is :

Listener 1 Listener 2 Microtask 1 Microtask 2

enter image description here

Here's the code:

window.onload = function(){
    document.getElementById("myBtn").addEventListener('click', () => {
        Promise.resolve().then(() => console.log('Microtask 1'));
        console.log('Listener 1');
    } );

    document.getElementById("myBtn").addEventListener('click', () => {
        Promise.resolve().then(() => console.log('Microtask 2'));
        console.log('Listener 2');
} );
}
function clickB(){

    document.getElementById("myBtn").click();
}
<!DOCTYPE html>
<html>
<button id="myBtn">Manual Click</button>
<button onclick="clickB()">JS Click</button>
</html>

My understanding is that items from Task Queue and Microtask Queue only get pushed into the Call Stack when it's empty. I might have made wrong assumptions. Please feel free to correct me. Thank you

Kemerovo answered 11/12, 2021 at 17:17 Comment(3)
Probably don't know enough yet but, just taking a stab, when window loads, it sets the event listeners and then runs the code, the functionB then running both console.logs that were assigned oringally and then because that first function re-assigns the event listeners is like setting a variable. So given that javascript given priority to closure events first before the scope chain, that would be my guess is to why the JSclick btn runs the listener 1 and listener 2 logs first because the rest of the code is at is base, reassigning a variable. Probably way off.Reliquary
click() is a wrapper around dispatchEvent - which gets a return value from the event handlers, so it's clearly not queueing a task. Try adding two console.log() statements around the .click() call and it'll become clear.Arrhenius
Yes it makes sense now. In fact I read one of your answers related to this which already cleared my doubts this one. Thanks @ArrheniusKemerovo
S
6

Your observations are correct and the explanation is quite straightforward:
The microtask queue only gets visited when the JavaScript execution context stack (a.k.a. call-stack) is empty, (defined in cleanup after running a script, itself called by call a user's operation). Technically the event loop has quite a few calls to perform a microtask checkpoint, but the cases where the JS call-stack can be non-empty are so rare they can be disregarded.

Dispatching an Event through eventTarget.dispatchEvent() is synchronous, no new task is queued, the callbacks are just called from the current context, and moreover, the JS call stack is not empty.

const target = new EventTarget();
target.addEventListener("foo", () => console.log("new foo event"));
console.log("before");
target.dispatchEvent(new Event("foo"));
console.log("after");

So the microtask queue doesn't get visited either, it will only be after the JS call stack is done, which in your code is when the original click event's handler job is executed completely.

However Events dispatched "natively" by the engine will create a new JS job per callback, and thus between each of them the JS call stack will be empty, and the microtask queue will get visited.

Senescent answered 12/12, 2021 at 2:17 Comment(1)
I had read about the EventTarget.dispatchEvent() and took references from these two answers as well : answer 1 and answer 2. I was about to post the answer myself but you explained everything so beautifully. It all makes sense now. Thanks @Kaiido.Kemerovo
Q
0

As long as the <button onclick> is running, the .then() won't be executed.

This snippet shows the difference in the execution order a bit better:

window.onload = function() {
  [document.body, myBtn].forEach(node => {
    node.addEventListener("click", function(e) {
      const {
        currentTarget,
        target
      } = e;
      new Promise((r) => {
        console.log(
          "Promise()",
          currentTarget.tagName
        )
        r();
      }).then(() => {
        console.log(
          ".then()",
          currentTarget.tagName
        )
      });
    });
  });
}
<button id="myBtn" onclick="console.log('hello world');">Manual Click</button>
<button onclick="myBtn.click(); console.log('I will happen before the then');">JS click</button>
Quinquepartite answered 11/12, 2021 at 19:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.