Search code examples
javascriptevent-looptask-queue

Which task(setTimeout or click event) is prioritized in the task queue?


I'm learning execution stack, task queue and event loop mechanism.

I continuously clicked the button until main thread is available (just before function a() is done) like below.
I thought click(UI) events and setTimeout uses the same queue, called Macrotask or Task Queue, so when I click the button between 1s and 3s, I thought the log of the task2 is printed between task1 and task2. But the result was not like that. Task2(click event) is always printed first and setTimeout events(task1,task3) are printed after click events.

So I'm wondering if click events are using different queue mechanism from setTimeout, or click event is prioritized over setTimeout.

thank you in advance for your help

Operation

  1. click the button (task2)
  2. --------1000ms setTimeout task1--------
  3. click the button (task2)
  4. --------3000ms setTimeout task3--------
  5. click the button (task2)
  6. --------6000ms main thread is now available--------

My Expectation Log Order

fn a done
task2 (click) done
task1 (setTimeout 1000ms) done
task2 (click) done
task3 (setTimeout 3000ms) done
task2 (click) done

Result Log Order

fn a done
task2 (click) done
task2 (click) done
task2 (click) done
task1 (setTimeout 1000ms) done
task3 (setTimeout 3000ms) done

Code

const btn = document.querySelector('button');
btn.addEventListener('click', function task2() {
  console.log('task2 (click) done');
});

function a() {
  setTimeout(function task1() { 
    console.log('task1 (setTimeout 1000ms) done');
  }, 1000);

  setTimeout(function task3() { 
    console.log('task3 (setTimeout 3000ms) done');
  }, 3000);

  // hold main thread for 6000ms(*1)
  const startTime = new Date();
  while (new Date() - startTime < 6000);

  console.log('fn a done');
}

a();
<button>button</button>
<script src="main.js"></script>


Solution

  • I thought click(UI) events and setTimeout uses the same queue

    They don't.

    UI events use the user interaction(UI) task source, which in most browsers has its own task-queue, setTimeout uses the timer task-source which also has its own task-queue in most browsers.

    Although it is not required by specs, UI task source has one of the highest priority of all task sources in almost all browsers, and timers have one of the lowest.

    How this prioritization works is that at the first step of the event-loop's processing model, the user-agent(UA) has to pick in one of its event-queues which task it will execute.

    Note: What is colloquially called a "macro-task" is any task that is not a microtask.
    The microtask queue is not a task queue and while a microtask can be chosen as the main task in the first step of the event loop processing, the microtask queue can't be prioritized because it has to be emptied synchronously at each microtask-checkpoints, which can happen several times during each event-loop iteration, and particularly every time the JS callstack is emptied.

    So here, when the while loop is done blocking the event-loop, and the next iteration starts, the UA will have to choose from which task-queue it should pick the next task. It will see in its UI task queue that there are new events waiting, and execute them because they have an higher priority.
    Then when they're all done being executed, it will choose the timers queue and execute them in the order of their scheduled time.

    Note also that they have a starvation system which prevents an high priority task-queue to block other task-queues for too long.


    Finally, I should mention there is a proposal to let us web-devs deal directly with all this prioritization thing: main thread scheduling.

    Using this experimental feature, we could rewrite the snippet as

    if( !("scheduler" in window) ) {
      console.error("Your browser doesn't support the postTask API");
      console.error("Try enabling the Experimental Web Platform features in chrome://flags");
    
    }
    else {
      scheduler.postTask(() => { 
        console.log('task1 (background) done');
      }, { priority: "background" } );
    
      scheduler.postTask(() => { 
        console.log('task2 (background) done');
      }, { priority: "background" } );
    
      // hold main thread for 6000ms(*1)
      const startTime = new Date();
      while (new Date() - startTime < 2000);
      scheduler.postTask(() => { 
        console.log('task3 (user-blocking) done');
      }, { priority: "user-blocking" } );
    
      console.log('synchronous done');
    }

    And see that the final task is executed first.