Onex
Onex

Reputation: 73

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

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

Upvotes: 6

Views: 1683

Answers (2)

Kaiido
Kaiido

Reputation: 136755

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.

Upvotes: 6

Christopher
Christopher

Reputation: 3667

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>

Upvotes: 0

Related Questions