I read some files about the microtask and macrotask queue. I know that the callback function of a resolved promise is added to the microtask, same as the other sync tasks. But the following code:
fetch=() => new Promise ((resolve) => resolve ({m: 121}))
function x() {
let d = 3;
fetch().then((data) => {
d += data.m;
console.log("b", d);
});
console.log("a", d);
}
x();
gives a result of
a 3
b 124
IAs I thought, since fetch here fetches a "local" file and it takes no time, so the callback function is immediately added to the microtask queue, and then console.log("a", d); is added to the microtask queue. If so, the result should be
b 124
a 124
So what is the reason for the real result? Why the call back function is executed after console.log("a", d);?
Whether or not a callback is added to the microtask queue or the callback queue has nothing to do with how long it takes to run. The Promise callbacks are always added to the microtask queue. Regardless of how long it takes. And, the queue is cleared, after the call stack is empty. In your example, the function x has to complete execution before your Promise callback can run. So, a is logged first.
Read more about the event loop here. But in a nutshell, browser API callbacks go in the callback queue, and higher priority callbacks like the Promise callbacks go in the microtask queue. The queues are cleared only after the call stack is empty.
This can lead to puzzling behaviors like the following code running forever since the call stack never becomes empty.
let flag = true;
setTimeout(() => flag = false, 200)
while (flag) console.log('hello')
Related
I am not sure how statements in the global scope are placed into the JavaScript event queue. I first thought that the interpreter went through and added all global statements into the event queue line by line, then went and executed each event, but that logic does not line up with the example given below. How does the JavaScript interpreter add global statements to the event queue, and why is the output from the two examples given below different?
let handleResolved = (data) => {
console.log(data);
}
let p = new Promise((resolve, reject) => {
setTimeout(() => {resolve("1")}, 0)
});
p.then(handleResolved);
setTimeout(() => {
console.log("2");
}, 0);
The console output to the above code is
1
2
Now consider this example. Here, the difference is on the body of the promise callback, as there is a nested setTimeout
let handleResolved = (data) => {
console.log(data);
}
let p = new Promise((resolve, reject) => {
setTimeout(() = > {setTimeout(() => {resolve("1")}, 0)}, 0);
});
p.then(handleResolved);
setTimeout(() => {
console.log("2");
}, 0);
The console output to the above code is
2
1
What I don't understand is the order in which things are added to the event queue. The first snippet implies that the promise p will run, and then during its execution, resolve is put in the event queue. Once all of p's stack frames are popped, then resolve is run. After than p.then(...) is run, and finally the last console.log("2");
In the second example, somehow the number 2 is being printed to the console before the number 1. But would things not be added to the event queue in this order
1.) p
2.) setTimeout( () => {resolve("1")}, 0)
3.) resolve("1")
4.) p.then(...)
5.) console.log("2")
I clearly have some sort of event queue logic wrong in my head, but I have been reading everything I can and I am stuck. Any help with this is greatly appreciated.
There are several confusing things in your question that I think show some misconceptions about what is happening so let's cover those initially.
First, "statements" are not ever placed into the event queue. When an asynchronous task finishes running or when it is time for a timer to run, then something is inserted in the event queue. Nothing is in the queue before that. Right after you call setTimeout(), before the time has come for the setTimeout() to fire there is nothing in the event queue.
Instead, setTimeout() runs synchronously, configures a timer in the internals of the JS environment, associates the callback function you passed to setTimeout() to that timer and then immediately returns where JS execution continues on the next line of code. Sometime later when the time has been reached for the timer to fire and control has returned back to the event loop, the event loop will call the callback for that timer. The internals of exactly how this works vary a bit according to which Javascript environment it is, but they all have the same effect relative to other things going on in the JS environment. In nodejs, for example, nothing is ever actually inserted into the event queue itself. Instead, there are phases of the event loop (different things to check to see if there's something to run) and one of the phases is to check to see if the current time is at or after the time that the next timer event is scheduled for (the soonest timer that has been scheduled). In nodejs, timers are stored in a sorted linked list with the soonest timer at the head of the list. The event loop compares the current time with the timer on the timer at the head of the list to see if its time to execute that timer yet or not. If not, it goes about its business looking for other types of events in the various queues. If so, it grabs the callback associated with that timer and calls the callback.
Second, "events" are things that cause callback functions to get called and the code in that callback function is executed.
Calling a function that may then cause something to be inserted into the event queue, either immediately or later (depending upon the function). So, when setTimeout() is executed, it schedules a timer and some time later, it will cause the event loop to call the callback associated with that timer.
Third, there is not just a single event queue for every type of event. There are actually multiple queues and there are rules about what gets to run first if there are multiple different types of things waiting to run. For example, when a promise is resolved or rejected and thus has registered callbacks to call, those promise jobs get to run before timer related callbacks. Promises actually have their own separate queue for resolved or rejected promises waiting to call their appropriate callbacks.
Fourth, setTimeout(), even when given a 0 time, always calls its callback in some future tick of the event loop. It never runs synchronously or immediately. So, the rest of the current thread of Javascript execution always finishes running before a setTimeout() callback ever gets called. Promises also always call .then() or .catch() handlers after the current thread of execution finishes and control returns back to the event loop. Pending promise operations in the event queues always get to run before any pending timer events.
And to confuse things slightly, the Promise executor function (the callback fn you pass as in new Promise(fn)) does run synchronously. The event loop does not participate in running fn there. new Promise() is executed and that promise constructor immediately calls the executor callback function you passed to the promise constructor.
Now, lets look at your first code block:
let handleResolved = (data) => {
console.log(data);
}
let p = new Promise((resolve, reject) => {
setTimeout(() => {resolve("1")}, 0)
});
p.then(handleResolved);
setTimeout(() => {
console.log("2");
}, 0);
In order, here's what this does:
Assign a function to the handleResolved variable.
Call new Promise() which immediately and synchronously runs the promise executor callback you pass to it.
That executor callback, then calls setTimeout(fn, 0) which schedules a timer to run soon.
Assign the result of the new Promise() constructor to the p variable.
Execute p.then(handleResolved) which just registers handleResolved as a callback function for when the promise p is resolved.
Execute the second setTimeout() which schedules a timer to run soon.
Return control back to the event loop.
Shortly after returning control back to the event loop, the first timer you registered fires. Since it has the same execution time as the 2nd one you registered, the two timers will execute in the order they were originally registered. So, the first one calls its callback which calls resolve("1") to cause the promise p to change its state to be resolved. This schedules the .then() handlers for that promise by inserting a "job" into the promise queue.
That job will be run after the current stack frame finishes executing and returns control back to the system.
The call to resolve("1") finishes and control goes back to the event loop.
Because pending promise operations are served before pending timers, handleResolved(1) is called. That functions runs, outputs "1" to the console and then returns control back to the event loop.
The event loop then calls the callback associated with the remaining timer and "2" is output to the console.
What I don't understand is the order in which things are added to the event queue. The first snippet implies that the promise p will run, and then during its execution, resolve is put in the event queue. Once all of p's stack frames are popped, then resolve is run. After than p.then(...) is run, and finally the last console.log("2");
I can't really respond directly to this because this just isn't how things work at all. Promises don't "run". The new Promise() constructor is run. Promises themselves are just notification machines that notify registered listeners about changes in their state. resolve is not put in the event queue. resolve() is a function that gets called and changes the internal state of a promise when it gets called. p doesn't have stack frames. p.then() is run immediately, not later. It's just that all that p.then() does is register a callback so that callback can then be called later. Please see the above 1-11 steps for the sequence of how things work.
In the second example, somehow the number 2 is being printed to the console before the number 1. But would things not be added to the event queue in this order
In the second example, you have three calls to setTimeout() where the third one is nested inside the first one. This is what changes your timing relative to the first code block.
We have mostly the same steps as the first example except that instead of this:
setTimeout(() => {resolve("1")}, 0)
you have this:
setTimeout(() = > {setTimeout(() => {resolve("1")}, 0)}, 0);
This means that the promise constructor is called and this outer timer is set.
then, the rest of the synchronous code runs and the last timer in the code block is then set. Just like in the first code block, this first timer will get to call its callback before the second one. But, this time the first one just calls another setTimeout(fn, 0). Since timer callbacks are always executed in some future tick of the event loop (not immediately, even if the time is set to 0), that means that all the first timer does when it gets a chance to run is schedule another timer. Then, the last timer in the code block gets it's turn to run and you see the 2 in the console. Then, when that's done, the third timer (the one that was nested in the first timer) gets to run and you see the 1 in the console.
If we break down the second case so that each function is on its own we end up with
const handleResolved = (data) => {
console.log(data);
}
const promiseBody = (resolve, reject) => setTimeout( innerPromiseTimeout, 0, resolve );
const innerPromiseTimeout = (resolve) => setTimeout( resolveWith1, 0, resolve );
const resolveWith1 = (resolve) => resolve("1");
const timeoutLog2 = () => {
console.log("2");
};
// beginning of execution
// timers stack: [ ]
// promiseBody is executed synchronously
let p = new Promise( promiseBody );
// timers stack: [ innerPromiseTimeout ]
// this will happen only after resolveWith1 is called
p.then( handleResolved );
// timers stack: [ innerPromiseTimeout ]
setTimeout( timeoutLog2, 0 );
// timers stack: [ innerPromiseTimeout, timeoutLog2 ]
// some time later, innerPromiseTimeout is called
// timers stack: [ timeoutLog2, resolveWith1 ]
// timeoutLog2 is called
// timers stack: [ resolveWith1 ]
// resolveWith1 is called and then is executed in next microtask checkpoint
// timers stack: [ ]
Also note that setTimeout still has a minimum of 1ms in Chrome (they will soon remove it, but for the time being, it's there), so don't assume setTimeout(fn,0) will execute as the next task
I was learning promises in JS and got curious on how promises work with Job queues behind the scenes. To explain my confusion I want to show you this code:
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
})
If you look at the above code, is it true that the callback of then() is put into Job queue beforehand and waits for promise to resolve? Or Is it true that callback of then() is pushed into job queue only after promise gets resolved?
When it's time to call a promise callback, the job doesn't go on the standard job queue (ScriptJobs) at all; it goes on the PromiseJobs queue. The PromiseJobs queue is processed until it's empty when each job from the ScriptJobs queue ends. (More in the spec: Jobs and Job Queues.)
I'm not sure what output you were expecting from your code as you didn't say, but let's take a simpler example:
console.log("top");
new Promise(resolve => {
setTimeout(() => {
console.log("timer callback");
}, 0);
resolve();
})
.then(() => {
console.log("then callback 1");
})
.then(() => {
console.log("then callback 2");
});
console.log("bottom");
The output of that, reliably, is:
top
bottom
then callback 1
then callback 2
timer callback
because:
The ScriptJobs job to run that script runs
console.log("top") runs
The promise executor function code runs, which
Schedules a timer job for "right now," which will go on the ScriptJobs queue either immediately or very nearly immediately
Fulfills the promise (which means the promise is resolved before then is called on it) by calling resolve with no argument (which is effectively like calling it with undefined, which not being thenable triggers fulfillment of the promise).
The first then hooks up the first fulfillment handler, queuing a PromiseJobs job because the promise is already fulfilled
The second then hooks up the second fulfillment handler (doesn't queue a job, waits for the promise from the first then)
console.log("bottom") runs
The current ScriptJob job ends
The engine processes the PromiseJobs job that's waiting (the first fulfillment handler)
That outputs "then callback 1" and fulfills the first then's promise (by returning)
That queues another job on the PromiseJobs queue for the callback to the second fulfillment handler
Since the PromiseJobs queue isn't empty, the next PromiseJob is picked up and run
The second fulfillment handler outputs "then callback 2"
PromsieJobs is empty, so the engine picks up the next ScriptJob
That ScriptJob processes the timer callback and outputs "timer callback"
In the HTML spec they use slightly different terminology: "task" (or "macrotask") for ScriptJobs jobs and "microtask" for PromiseJobs jobs (and other similar jobs).
The key point is: All PromiseJobs queued during a ScriptJob are processed when that ScriptJob completes, and that includes any PromiseJobs they queue; only once PromiseJobs is empty is the next ScriptJob run.
I would say callback of then() is pushed into job queue only after promise gets resolved.
If you changed the first timeout to 3000, you run the code and it waits until 3's to alert 1. This is because you have to wait the promise to be resolved in 3 seconds.
You get get the answer from here: https://stackoverflow.com/a/30910084/12733140
promiseA.then()'s callback is a task
promiseA is resolved/rejected: the task will be pushed into microtask queue in current round of event loop.
promiseA is pending: the task will be pushed into microtask queue in the future round of event loop(may be next round)
So here microtask is the same as "job" as you mentioned above, only the promise is resolved or rejected, the callback will be pushed to job/microtask queue.
I recently watched this video: https://www.youtube.com/watch?v=8aGhZQkoFbQ and am trying to find a similar explanation for with Promises. Specifically in node.
I'm probably wrong but I'm under the impression that Promises are placed on a microtask queue, the microtask queue is only processed when the callstack is empty and once the event-loop starts processing the microtask queue, it processes it until the queue is empty.
const sleep = (ms) => new Promise((res) => {
setTimeout(res, ms)
});
const doTask1 = (workerId) => sleep(1000).then(() => console.log(`[${workerId}] Completed Task 1`));
const doTask2 = (workerId) => sleep(5000).then(() => console.log(`[${workerId}] Completed Task 2`));
const worker = async (workerId) => {
console.log(`[${workerId}] Starting work`)
await doTask1(workerId);
await doTask2(workerId);
console.log(`[${workerId}] Completed all tasks`);
};
worker('A');
worker('B');
When the snippet above is run, both worker A and B will start Task 1 and 2 at around the same time and complete at around the same time. I'm having trouble consolidating that behavior with what I've read/seen about Promises/microtasks and event-loops.
I imagine that when the code executes, the call stack looks like:
Callstack: [main.js]
Callstack: [main.js][worker('A')]
Callstack: [main.js][worker('A')][console.log()]
Callstack: [main.js][worker('A')][await doTask1()]
When we use await, what happens to the callstack? I suppose this is related to how generators work?
Callstack: ?? | Microtask Queue: [doTask1()]
What does it mean for the event-loop to "process" the microtask queue? Does processing doTask1() mean the event-loop is waiting 1 second? That doesn't sound right. Is the 1 second wait being done on a different thread (is that the right term?) as shown in the Youtube video's depiction of WebAPIs? Once the timeout is complete, the Promise's res goes onto the message queue where the event loop eventually picks it up and resolves the Promise? Does the callstack have to be empty for the callstack to execute res?
I'm stuck at this point and not sure what happens next in the callstack, the message queue, the microtask queue and how the event-loop decides what to do next.
The following quotes are my primary references for understanding microtask queue processing:
Microtasks (which promises use) are processed when the JS stack empties.
- Jake Archibald
That doesn't make sense to me.
One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification). After this macrotask has finished, all available microtasks will be processed, namely within the same go-around cycle.
- Stack Overflow
Now, regarding line 9 (**) in the following snippet:
From stepping through this snippet w/ debugger, the execution stack does not appear empty when these .then( callback ) microtasks are processed/executed.
Are regular functions like f2() considered a task (aka macrotask)? (when it returns it's an event loop nextTick() and the microtask queue is processed)
Why are microtasks executing when the JS stack is not empty?
function f2() {
let x = new Promise( (resolve, reject) => { resolve( () => {console.log('howdy')} ) })
return x
}
async function f1(){
let y = Promise.resolve().then(() => { console.log('yo1')})
console.log('yo2')
let r2awaited = await f2() //** 'yo0' and 'yo1' log when the interpreter hits this line.
return r2awaited
}
async function start(){
let y = Promise.resolve().then(() => { console.log('yo0')})
let xx = await f1()
console.log('main call return:')
console.log(xx)
}
start()
Edit: Another peculiar finding - when you add xx() right after console.log(xx) on line 17, the stack is completely cleared prior to executing the microtasks.
The call stack 1 step prior to microtask queue processing:
Then the immediate next step.
Between these two steps, the microtask queue was processed.
Does the call stack clear [under the hood] between these steps^?
And then is a new call stack created according to the required lexical environment(s) for code after the await [expression]?
Edit: At the time of posting this, I was not aware that everything below the -----(async)----- line in the chrome debugger's call stack was part of a 'fake stack'.
This 'fake stack' is presented for async debugging in a way consistent with sync debugging.
Only the elements above this -----(async)----- line are part of the real main thread call stack.
"Microtasks (which promises use) are processed when the JS stack empties." -Jake Archibald (doesn't make sense to me)
The "call stack" is the list of things that are currently executing:
function foo() {
debugger;
console.log('foo');
}
function bar() {
foo();
debugger;
}
bar();
When we hit the first debugger statement, the script is still executing, as is bar, as is foo. Since there's a parent-child relationship, the stack is script > bar > foo. When we hit the second debugger statement, foo has finished executing, so it's no longer on the stack. The stack is script > bar.
The microtask queue is processed until it's empty, when the stack becomes empty.
"One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification). After this macrotask has finished, all available microtasks will be processed, namely within the same go-around cycle." - stackoverflow
Edit: I kept reading "macrotask" above as "microtask". There isn't really such a thing as a macrotask queue in the browser, it's just a task queue.
Although it's true that there's a microtask processing point after processing a task, it's only really there to handle specifications that queue tasks to queue microtasks, without calling into JS first. Most of the time, the microtask queue is emptied when the JS stack empties.
From stepping through this snippet w/ debugger, the execution stack does not appear empty when these .then( callback ) microtasks are processed/executed.
The stack will never be empty while callbacks are being executed, since the callback itself will be on the stack. However, if this is the only thing on the stack, you can assume the stack was empty before this callback was called.
Chrome's devtools tries to be helping in maintaining an "async" stack, but this isn't the real stack. The real stack is everything before the first "async" line.
Are regular functions like f2() considered a task
Being a task or a microtask isn't a property of a function. The same function can be called within a task, a microtask, and other parts of the event loop such as rendering. Eg:
function foo() {}
// Here, I'll call foo() as part of the current task:
foo();
// Here, I'll let the browser call foo() in a future task:
setTimeout(foo);
// Here, I'll let the browser call foo() in a microtask:
Promise.resolve().then(foo);
// Here, I'll let the browser call foo() as part of the render steps:
requestAnimationFrame(foo);
In your example, f2 is not called within a microtask. It's kinda like this:
function three() {}
function two() {}
async function one() {
await two();
three();
}
one();
Here, one() is called within the task that executed the script. one() calls two() synchronously, so it runs as part of the same task. We then await the result of calling two(). Because we await, the rest of the function runs in a microtask. three() is called, so it runs in the same microtask.
I'm having trouble understanding why my code below logs the following in order:
"end"
"timeout done"
"promise"
I assumed that "promise" would log before "timeout done" since it has a priority over callback queue task (setTimeout).
My assumption after this observation is that until .then is called, the promise does not enqueue its task since it's not ready yet, and thus allows setTimeout to perform in the callback queue to be performed first. Is this correct?
const sampleFunction = function(e) {
setTimeout(() => console.log('timeout done'), 0)
const data = fetch(`https://jsonplaceholder.typicode.com/comments/1`)
.then(response => {
return response.json();
})
.then(json => {
/*doSomething*/
console.log('promise')
});
console.log('end')
}
Your console.log('promise') has to wait until your fetch().then() is done which involves a networking operation to another host (and thus non-zero time).
Your setTimeout() only has to wait until sampleFunction() returns and you get back to the event queue. So, since the fetch().then() is non-blocking and takes non-zero amount of time, when you first get back to the event queue, only the setTimeout() is ready to go. The networking operation will still be in process in the background.
So, this isn't a matter of prioritization. It's a matter of completion order. The setTimeout() inserts its completion event into the event queue long before the fetch() promise is resolved.
Perhaps you didn't realize that fetch() is non-blocking and calling it just initiates the operation and then your code execution continues after that?
In the console, you should see:
end
timeout done
promise
As long as your fetch() doesn't have any errors.