Reputation: 31
I was recently watching a workshop video by Will Sentence on async javascript and I am having trouble understanding whether microtask queue always has priority over macrotask queue.
function display(data) {console.log(data)}
function printHello() {console.log(“Hello”);}
function blockFor300ms() {/* blocks js thread for 300ms with long for loop */}
setTimeout(printHello, 0);
const futureData = fetch('https://twitter.com/will/tweets/1')
futureData.then(display)
blockFor300ms()
console.log(“Me first!”);
In this example, Will explains that printhello
is added to the macrotask (callback) queue then the fetch
request gets executed and suppose at 201ms of the code exuction, the display
callback is added to the microtask queue. Meanwhile the blockFor300ms
had blocked the code for 300ms.
At about 301ms Me first!
gets console logged, then since the microtask queue has priority over the macrotask queue, the data
from the fetch request gets logged and finally the setTimeout's Hello
. So the order of the console.logs:
1. console.log("Me first!")
2. console.log(data) // data from fetch request
3. console.log("Hello")
I tried executing similar code example in various environments (Chrome, Firefox, Safari, node), with various blocker functions (with varying durations) and I always get:
1. synchronous code console.log("Me first!")
2. console.log from the setTimeout
3. console.log with data from fetch request .then
I thought maybe the result depends on when the fetch request gets resolved and the data is received but I have also tried blocking the code for more than 10 or 20 seconds and the outcome is always setTimeout first then the fetch. Whats going on here and what's the reason behind this?
Here is the code I tested with:
function display(data) {console.log("Fetched data:", data);}
function printHello() {console.log("Hello")}
function blockForDuration(duration) {
const startTime = Date.now();
while (Date.now() - startTime < duration) {}
}
setTimeout(printHello, 0);
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(display)
.catch(error => console.error("Fetch error:", error));
blockForDuration(10000);
console.log("Me first!");
/*
Console log order
1. "Me first!"
2. "Hello"
3. "Fetched data:", data
*/
Upvotes: 2
Views: 133
Reputation: 413682
I think this is clearer if you write the code with async
and await
, because it's easier to get "in between" the things that happen:
!async function() {
function display(data) {console.log("Fetched data:", data);}
function printHello() {console.log("Hello")}
function blockForDuration(duration) {
const startTime = Date.now();
while (Date.now() - startTime < duration) {}
}
setTimeout(printHello, 0);
const p = fetch('http://jsonplaceholder.typicode.com/posts/1');
console.log("back from fetch with promise");
const pr = await p;
console.log("fetch initial promise resolved");
const j = await pr.json();
console.log("have JSON");
display(j);
blockForDuration(10000);
console.log("Me first!");
}();
In this version, there's a log message after fetch()
returns but before the promise is awaited. Note that the fetch()
request has to take some time, but the Promise comes back pretty much immediately, so that's the first thing logged.
The next thing logged is "Hello" from the setTimeout()
, which runs in a macrotask that happens after the fetch()
starts doing its real work and the function is suspended. That's what's happening in the first call to .then()
in the original. So the .then()
callback in the original runs in a microtask, but its in a microtask after a different macrotask; it's basically when the HTTP request completes. That's an asynchronous process, and for that matter so is the .json()
call. The Promise resolutions are on their own macrotask (when .then()
actually runs and queues up the callback); it's the callback that runs in the microtask.
Note that the await
version logs the "Me first" message last, because of the nature of await
. That code is effectively in a .then()
that's synthesized after other stuff. You could probably rearrange the await
version so that it's more like the original.
edit — here's a version much closer to the original:
function display(data) {console.log("Fetched data:", data);}
function printHello() {console.log("Hello")}
function blockForDuration(duration) {
const startTime = Date.now();
while (Date.now() - startTime < duration) {}
}
setTimeout(printHello, 0);
const p1 = fetch('https://jsonplaceholder.typicode.com/posts/1');
console.log("got first fetch promise, now calling .then()");
const p2 = p1.then(response => (console.log("first promise resolved"), response.json())).then(data => (console.log("JSON resolved"), display(data)));
blockForDuration(10000);
console.log("Me first!");
This stuff is interesting and really good to know, in a "back of your head" kind of way, but my personal advice would be to shy away from making significant architectural decisions based on the little details of how the task mechanisms interact. I won't say there aren't situations where it might be very important, but in most day-to-day web programming it's little more than an interesting detail.
Upvotes: -1