简体   繁体   中英

event loop prefers microtask queue over callback queue?

I was testing out concepts of asynchronous code in JS . Got confused between callback queue & microtask queue order. Whenever promise objects gets resolved , the fulfillment method { then } is pushed into microtask queue while the callbacks of browser timer functions such as setTimeout is pushed into callback queue. Event loop continuously checks queue and pushes functions from queue into call stack whenever call stack gets empty. Event loop should prefer microtask queue over normal callback queue but in the example : https://jsfiddle.net/BHUPENDRA1011/2n89ftmp/ it's happening otherwise.

function display(data) {
    console.log('hi back from fetch');
}

function printHello() {
    console.log('hello');
}

function blockfor300ms() {
    for (let i = 0; i < 300; i++) {
        // just delaying logic
    }
}
// this sets the printHello function in callback queue { event loop queue }
setTimeout(printHello, 0);

const futureData = fetch('https://api.github.com/users/xiaotian/repos');
// after promise is resolved display function gets added into other queue : Microtask queue { job queue}
futureData.then(display);
// event loop gives prefrence to Microtask queue ( untill  its complete) 

blockfor300ms();
// which runs first 
console.log('Me first !')

expected output

  • Me first !
  • hi back from fetch
  • hello

actual output :

  • Me first !
  • hello
  • hi back from fetch

Kindly let me know how it's happening over here.

Thanks

While it is true, what "kib" stated:

"your function blockfor300ms doesn't block the thread long enough for the fetch to receive a response"

sadly this is irrelevant, because even if you did block execution until after you received a response to your fetch call, you would still see the same result... (see sample code snippet below, you can block execution with an alert box or a long loop of non-async XMLHttpRequest calls, I received the same result)

Unfortunately, fetch does not work as described by all the blogs and resources I've found... which state that

Fetch will add it's promise chain to the micro-tasks queue and run before any callbacks on the next tick of the event loop.

From the sample code below, it appears fetch does not simply add it's resolve handler function to the micro-tasks queue as described by others, because as Lewis stated since it requires network activity it is being handled by the Network Task Source. But, I don't believe this is due to printHello "blocking" the network task, because fetch is being fired prior to printHello in my sample code below, and the network response would occur before the timer completed as well.

As you can see in the example below I have the printHello delayed to be added to the Task Queue long after the fetch response has been received (2000ms), yet if we block the code execution for longer than 2000ms (so that there is still running execution context) then "Hello" will be printed first. Meaning the fetch resolve() handler is NOT being simply added to the Micro-Task Queue where it would have fired along with the other promise handlers.

So, why is it still being logged after the callback if the response is received and theoretically added to the Task Queue prior to the timer task completing (at 2000ms)? Well, my guess is that the timer task source must be receiving priority over that of the network task source. And therefore both are sitting in their task queue, but the timer task queue is firing before the network task queue...

Links to specs:

Timer Task Source - https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-task-source Networking Task Source - https://html.spec.whatwg.org/multipage/webappapis.html#networking-task-source

 function display(data){console.log("Fetch resolved!")} function printHello(){console.log("Callback Time")} function blockExecution() { console.log("Blocking execution..."); alert('Wait at least 2000ms before closing'); } const futureData = fetch('https://jsonplaceholder.typicode.com/todos/1'); futureData.then(response => response.json()).then(display); setTimeout(printHello, 2000); const p = new Promise( // this is called the "executor" (resolve, reject) => { console.log("I'm making a promise..."); resolve("Promise resolved!"); console.log("Promise is made..."); } ); p.then( // this is called the success handler result => console.log(result) ); blockExecution(); console.log("Execution ended!");

futureData is actually a fetch promise so there is absolutely a network task queued into task queue when fetch is called. As the result, printHello will definitely be executed before the network task since they're both tasks. And method display will only be put into microtask queue when the promise of network task is resolved. Microtasks, by definition, are only executed at the end of each task. So display will be called at the end of the network task when printHello has already been called long time before.

If you want display to be called before printHello , futureData must only queue microtasks. Let's modify your example a bit.

 function display(data) { console.log('hi back from fetch'); } function printHello() { console.log('hello'); } let now = Date.now(); function executeFutureDataWithMicrotasksOnly() { // Execute microtasks continually in 300ms. return Promise.resolve().then(() => Date.now() - now < 300 && executeFutureDataWithMicrotasksOnly()); } function blockfor300ms() { for (let i = 0; i < 300; i++) { // just delaying logic } } // this sets the printHello function in callback queue { event loop queue } setTimeout(printHello, 0); const futureData = executeFutureDataWithMicrotasksOnly(); // after promise is resolved display function gets added into other queue : Microtask queue { job queue} futureData.then(display); // event loop gives prefrence to Microtask queue ( untill its complete) blockfor300ms(); // which runs first console.log('Me first !')

As you can see from the above example, if you replace fetch with a method having only microtasks, the execution order is changed as expected though both fetch and executeFutureDataWithMicrotasksOnly are executed in a similar time interval. When futureData no longer queues tasks, all microtasks including display will be executed at the end of the currently executing task, which is the previous task of task printHello .

I think the problem comes from the fact that your function blockfor300ms doesn't block the thread long enough for the fetch to receive a response. There won't be anything in the job queue (yet) when the event loop will see that it can call printHello .

Using Visualization

This will help you in giving a better understanding of how javascript works.

Here in this case fetch( Promise ) is taking more time than setTimeout so when the event loop cycle is running fetch( Promise ) was still in progress and setTimeout is executed first as it took less time and came out of the task queue and it gets processed and when fetch( Promise ) ends it comes out of the microtask queue.

If you will increase setTimeout time then the first fetch( Promise ) will occur first and then setTimeout will occur. Hope this solves your question.

It seems to be easier than we all think:

It is possible for a microtask to be moved to a regular task queue, if, during its initial execution, it spins the event loop. HTML Living Standard #queue a microtask

When the loop is in the process of selecting a task form the task queue, it can choose to execute tasks that were previously queued into the microtask queue and now are part of task queue:

In that case, the task chosen in the next step was originally a microtask, but it got moved as part of spinning the event loop. HTML Living Standard #event loop processing model

Code that spins the loop is anything that includes parallel operations:

  1. In parallel:
  1. Wait until the condition goal is met.

HTML Living Standard #spin-the-event-loop

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM