Home > Mobile >  Why are two setTimeouts not in the same event loop
Why are two setTimeouts not in the same event loop

Time:01-14

setTimeout(() => new Promise((r) => {console.log(1);r();}).then(() => console.log('1 mic')))
setTimeout(() => new Promise((r) => {console.log(2);r();}).then(() => console.log('2 mic')))
1
1 mic
2
2 mic

Why are two setTimeouts not in the same event loop

I didn't find an understandable answer after Google

CodePudding user response:

As I stated in the Community Wiki answer, the simple reason is that calling setTimeout(fn) will, after the timeout expires, queue a new task to execute the callback.
So each call to setTimeout will queue its own task. [Specs]

But I have to note that your test wouldn't tell you if two callbacks are ran in the same event loop iteration anyway.
Indeed, every time* a callback is invoked, a microtask checkpoint is performed in the "cleanup after running a script" algorithm.

So even in a case where we'd have multiple callbacks firing in the same event loop iteration, we'd have a microtask checkpoint in between each callback. For instance, requestAnimationFrame() callbacks are all fired in the same event-loop iteration, as part of the "update the rendering" step of the event loop. But even there, microtasks will be fired:

requestAnimationFrame(() => {
  console.log(1); Promise.resolve().then(() => console.log(1, "micro"));
  // Try to include a new task in between
  setTimeout(() => { console.log("a new task"); });
  // block the event loop a bit so we ensure our timer should fire already
  const t1 = performance.now();
  while(performance.now() - t1 < 100) {}
});
requestAnimationFrame(() => {
  console.log(2); Promise.resolve().then(() => console.log(2, "micro"));
});

So using microtasks is not a good way of checking if two callbacks are fired in the same event loop iteration. To do so, you could try to schedule a new task, like I did in that example. But even this isn't bullet proof, because browsers do have quite complex task prioritization system in place and we can't be sure when they'll fire different types of task.

The best, in supporting browsers, is to use the still under development Prioritized task scheduler.postTask method. This allows us to post tasks with the highest priority, and thus we can check if two callbacks are indeed in the same event loop iteration or not. In browsers that don't support this API we have to resort to using a MessageChannel object which should be the closest way of post high priority tasks:

const postTask = globalThis.scheduler
  ? (cb) => scheduler.postTask(cb, { priority: "user-blocking" })
  : postMessageTask;
  
const test = (fn, label) => {
  return new Promise((res) => {
    let sameIteration = true;
    fn(() => {
      console.log(label, "first callback");
      // If this task is executed before the next callback
      // it means both callbacks weren't executed in the same iteration
      postTask(() => sameIteration = false);
      // block the event-loop a bit between both callbacks
      const t1 = performance.now();
      while(performance.now() - t1 < 100) {}
    });
     fn(() => {
      console.log(label, "second callback");
      console.log({ label, sameIteration });
      res();
    });
  });
};
test(setTimeout, "setTimeout")
.then(() => test(requestAnimationFrame, "requestAnimationFrame") );

// If scheduler.postTask isn't available, use a MessageChannel to post high priority tasks
function postMessageTask(cb) {
  const { port1, port2 } = (postMessageTask.channel ??= new MessageChannel());
  port1.start();
  port1.addEventListener("message", () => cb(), { once: true });
  port2.postMessage("");
}

* That is, if the callstack is empty, which is the case most of the time, with one of the only exceptions being callbacks for events dispatched programmatically.

CodePudding user response:

Because each timer callback has its own task scheduled. [Specs]

  • Related