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

Onex
  • 73
  • 4
  • 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. – Brent Dec 11 '21 at 17:49
  • 1
    `click()` is a wrapper around [`dispatchEvent`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/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. – Bergi Dec 12 '21 at 05:58
  • Yes it makes sense now. In fact I read one of your answers related to this which already cleared my doubts [this one](https://stackoverflow.com/a/33622043/14719174). Thanks @Bergi – Onex Dec 12 '21 at 07:33

2 Answers2

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.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • 1
    I had read about the [EventTarget.dispatchEvent()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent) and took references from these two answers as well : [answer 1](https://stackoverflow.com/a/22924862/14719174) and [answer 2](https://stackoverflow.com/a/33622043/14719174). I was about to post the answer myself but you explained everything so beautifully. It all makes sense now. Thanks @Kaiido. – Onex Dec 12 '21 at 07:29
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>
Christopher
  • 3,124
  • 2
  • 12
  • 29