How to observe multiple task queues in JS event loop [duplicate] - javascript

This question already has answers here:
Which types of queues are in event loop?
(3 answers)
Closed 2 years ago.
I'm reading an article that explains the event loop and execution timings. In it, it's mention that there can be multiple task queues that the event loop can choose to from to execute tasks. My main question is when does the browser decide to create a new queue? I've tried to observe this happening, but so far haven't been able to.
Contrasted with this other article on the subject, there's no mention of multiple queues, so I'm either misunderstanding something or one of the two articles is incorrect somewhere.

I believe the two articles in question are just using different terminology.
In article 1:
An event loop has multiple task sources which guarantees execution order within that source...
In article 2
the event loop can have multiple task queues... tasks must be processed in insertion order in every queue.
To answer my own question about observing different queues, I think it's as simple as this:
function foo() {
console.log('Start of queue');
bar();
Promise.resolve().then(function() {
console.log('Promise resolved');
});
console.log('End of queue');
}
function bar() {
setTimeout(function() {
console.log('Start of next queue');
console.log('End of next queue');
}, 0);
}
foo();
//-> Start of queue
//-> End of queue
//-> Promise resolved
//-> Start of next queue
//-> End of next queue
The first task queue (or task source) is foo(). foo() calls bar(), and bar() calls a setTimeout() which sets up a new task queue (or task source). The way we can observe each task queue is to resolve a Promise. The Promise callback is inserted into the micro queue. All micro queue tasks are executed at the end of every task queue (or task source). Because we see Promise resolved between the End of queue and Start of next queue console logs, we can conclude that that we're observing different event queues.

https://www.youtube.com/watch?v=u1kqx6AenYw&feature=youtu.be This will help you understand I think. Skip to the 7min mark.
There can be multiple queues in the task queue, and then also micro task queue.
In HTML terms, the event loop for a page or set of pages from the same domain can have multiple task queues. Tasks from the same task source always go into the same queue, with the browser choosing which task queue to use next.

Related

JavaScript async callbacks - Promise and setTimeout [duplicate]

This question already has answers here:
Promise vs setTimeout
(6 answers)
What is the relationship between event loop and Promise [duplicate]
(2 answers)
Closed 1 year ago.
In the following code:
setTimeout(() => console.log("hello"), 0);
Promise.resolve('Success!')
.then(console.log)
What should happen in my understanding:
setTimeout is called => print hello directly added to callback queue as time is 0
Promise.resolve => print Success! added to callback queue
If I am not wrong, the callback queue is FIFO.
But the code output is:
Success!
hello
What is the explanation?
There are 2 separate queues for handling of the callbacks. A macro and a micro queue. setTimeout enqueues an item in the macro queue, while promise resolution - to the micro queue. The currently executing macro task(the main script itself in this case) is executed synchronously, line by line until it is finished. The moment it is finished, the loop executes everything queued in the microtask queue before continuing with the next item from the macro queue(which in your case is the console.log("hello") queued from the setTimeout).
Basically, the flow looks like this:
Script starts executing.
MacrotaskQueue: [], MicrotaskQueue: [].
setTimeout(() => console.log("hello"), 0); is encountered which leads to pushing a new item in the macrotask queue.
MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [].
Promise.resolve('Success!').then(console.log) is read. Promise resolves to Success! immediately and console.log callback gets enqueued to the microtask queue.
MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [console.log('Success!')].
The script finishes executing so it checks if there is something in the microtask queue before proceeding with the next task from the macro queue.
console.log('Success!') is pulled from the microtask queue and executed.
MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [].
Script checks again if there is something else in the microtask queue. There is none, so it fetches the first available task from the macrotask queue and executes it, namely - console.log("hello").
MacrotaskQueue: [], MicrotaskQueue: [].
After the script finishes executing the console.log("hello"), it once again checks if there is anything in the microtask queue. It is empty, so it checks the macrotask queue. It is empty as well so everything queued is executed and the script finishes.
This is a simplified explanation, though, as it can get trickier. The microtask queue normally handles mainly promise callbacks, but you can enqueue code on it yourself. The newly added items in the microtask queue will still be executed before the next macrotask item. Also, microtasks can enqueue other microtasks, which can lead to an endless loop of processing microtasks.
Some useful reference resources:
The event loop
Microtasks
Using Microtasks
There are two different queues involved here: a Task queue and a Microtask queue.
Callback functions scheduled using setTimeout are added in the task queue whereas the callbacks scheduled using promises are added in the microtask queue or a job queue.
A microtask queue is processed:
after each callback as long as the call-stack is empty.
after each task.
Also note that if a microtask in a microtask queue queues another microtask, that will also be processed before processing anything in the task queue. In other words, microtask queue will be processed until its empty before processing the next task in the task queue.
The following code snippet shows an example:
setTimeout(() => console.log('hello'), 0);
Promise.resolve('first microtask')
.then(res => {
console.log(res);
return 'second microtask';
})
.then(console.log);
In your code, callback function of setTimeout is added to the task queue and the Promise.resolve queues a micro-task in a microtask queue. This queue is processed at the end of the script execution. That is why "success" is logged before "hello".
The following image shows a step-by-step execution of your code:
Resources for further reading:
Tasks, microtasks, queues and schedules
JavaScript job queue and microtasks
Even though the timeout is 0, the callback function will still be added to the web API (after being fetched from the call stack). Web APIs are threads that you can’t access; you can just make calls like Ajax, Timeout, and the DOM.
Promise.resolve schedules a microtask whereas setTimeout schedules a macrotask. Microtasks are executed before running the next macrotask.
So in your example, the
Promise.resolve('Success!').then(console.log);
will be executed before the setTimout since promises have better priority than the setTimeout callback function in the event loop stack.

How does the JS event loop behave when a Promise's resolution depends on setTimeout?

console.log('1')
setTimeout(() => {
console.log('2')
}, 0)
function three() {
return new Promise(resolve => {
setTimeout(() => {
return new Promise(resolve => resolve('3'))
},0)
})
}
three().then(result => console.log(result))
console.log('4')
This code snippet outputs 1 4 2
This is the behavior I would expect based on my understanding of javascript's event loop and concurrency model. But it leaves me with some lingering questions.
Before getting to those questions, I'll first break down my understanding of this code snippet.
Why code outputs 1
no explanation needed
Why code outputs 4
the callback that outputs 2 gets loaded into the event queue (aka macro task queue) after 0ms, but doesn't get executed until the main call stack is emptied.
even if three was a promise that was immediately resolved, its code is loaded into the job queue (aka microtask queue) and wouldn't be executed until the main call stack is emptied (regardless of the contents of the event queue)
Why code outputs 2
after console.log(4) the main call stack is empty and javascript looks for the next callback to load on the main stack. It's pretty safe to assume that at this point, some "worker thread" had already put the callback function that outputs 2 onto the macro task queue. This gets loaded onto the stack and 2 is output.
Why code does NOT output 3
This is where its a little blurry for me. The function three returns a promise that is then-ed in the main thread. Callback functions passed through then are loaded onto microtask queue and executed before the next task in the macrotask queue. So while you might think it'll run before the callback that logs 2, its actually theoretically impossible for it to run at all. That's because the Promise is only resolved via the callback function of its setTimeout, and that callback function (because of setTimeout) would only run if the main execution thread (the same thread that's waiting for the promise to resolve) is empty.
Why does this bother me
I'm trying to build a complete theoretical mental model of how javascript handles concurrency. One of the missing pieces in that model is the relationship between network requests, promises, and the event loop. Take the above code snippet, and suppose I replace three's setTimeout with some sort of network request (a very common thing in async web development). Assuming that the network request behaves similarly to setTimeout, in that when the "worker thread" is done, a callback is pushed to the macro task queue, it's hard for me to understand how that callback even gets executed. But this is something that happens literally all the time.
Can someone help me understand? Do I have any missing gaps in my current understanding of js concurrency? Have I made an incorrect assumption? Does any of this actually make any sense? lol
Why code does NOT output 3
In this code:
function three() {
return new Promise(resolve => {
setTimeout(() => {
return new Promise(resolve => resolve('3'))
},0)
})
}
three().then(result => console.log(result))
You never resolve the first Promise that three() creates. Since that's the one that is returned form three(), then the .then() handler in three().then(...) is never called.
You do resolve the promise created inside the timer, but you're returning that promise only to the timer callback which does nothing.
If you change your code to this:
function three() {
return new Promise(resolve => {
setTimeout(() => {
resolve('3');
},0)
})
}
three().then(result => console.log(result))
Then, you would see the 3get output.
So, this doesn't have anything to do with the event loop or how it works. It has to do with not resolving the promise that three() returns so the .then() handler on that promise never gets called.
I'm trying to build a complete theoretical mental model of how javascript handles concurrency. One of the missing pieces in that model is the relationship between network requests, promises, and the event loop. Take the above code snippet, and suppose I replace three's setTimeout with some sort of network request (a very common thing in async web development). Assuming that the network request behaves similarly to setTimeout, in that when the "worker thread" is done, a callback is pushed to the macro task queue, it's hard for me to understand how that callback even gets executed. But this is something that happens literally all the time.
Network requests and promises and timers all go through the event loop. There are very complicated rules about how multiple events in queue at the same time are prioritized relative to one another. .then() handlers are generally prioritized first.
Think of the Javascript interpreter as this simplistic sequence.
Get event from event queue
If nothing in the event queue, sleep until something is in the event queue
Run callback function associated with the event you pull from the event queue
Run that callback function until it returns
Note, it may not be completely done with its work because it may
have started other asynchronous operations and set up its own
callbacks or promises for those. But, it has returned from the
original callback that started it
When that callback returns, go back to the first step above and get the next event
Remember that network requests, promises, timers and literally ALL asynchronous operations in node.js go through the event queue in this manner.
Why you assume that network request would behave as setTimeout?
It is a Promise which resolved() would go in microtasks

Why does this line of code with 'await' trigger microtask queue processing?

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.

What does microtask runs after every callback mean?

I'm reading this article on microtasks and there is the following conclusion:
In summary:
Tasks execute in order, and the browser may render between them
Microtasks execute in order, and are executed:
after every callback,
as long as no other JavaScript is mid-execution at the end of each
task
As I understand each task represents a single VM turn, so as long as no other JavaScript is mid-execution at the end of each task means when call stack is empty. But I don't understand what after every callback mean?
Can anyone please explain and show an example?
Here is the clear example that demonstrates this:
function cb1() {
console.log('cb1');
Promise.resolve('df').then(function promiseMicrotask() {
console.log('promise');
});
}
function cb2() {
console.log('cb2');
}
const element = document.querySelector('div.inner');
element.addEventListener('click', cb1);
element.addEventListener('click', cb2);
In the example above when you click on the div.inner, a browser schedules a task to handle the event and call the callbacks cb1 and cb2. Later it starts executing the task and triggers cb1. Inside cb1 a resolved promise schedules a microtask to run the promiseMicrotask callback. Whenever the current stack that started with cb1 is empty the browser checks the microtaks queue and finds a microtask promiseMicrotask. It triggers it and so it logs promise. Then it proceeds to triggering cb2.
So the microtask promiseMicrotask was processed after the callback cb1 but before cb2 and before the browser finished executing all callbacks in the current task.
What's important here is that all event callbacks are executed in current task.

Difference between microtask and macrotask within an event loop context

I've just finished reading the Promises/A+ specification and stumbled upon the terms microtask and macrotask: see http://promisesaplus.com/#notes
I've never heard of these terms before, and now I'm curious what the difference could be?
I've already tried to find some information on the web, but all I've found is this post from the w3.org Archives (which does not explain the difference to me): http://lists.w3.org/Archives/Public/public-nextweb/2013Jul/0018.html
Additionally, I've found an npm module called "macrotask": https://www.npmjs.org/package/macrotask
Again, it is not clarified what the difference exactly is.
All I know is, that it has something to do with the event loop, as described in https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
and https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
I know I should theoretically be able to extract the differences myself, given this WHATWG specification. But I'm sure that others could benefit as well from a short explanation given by an expert.
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. While these microtasks are processed, they can queue even more microtasks, which will all be run one by one, until the microtask queue is exhausted.
What are the practical consequences of this?
If a microtask recursively queues other microtasks, it might take a long time until the next macrotask is processed. This means, you could end up with a blocked UI, or some finished I/O idling in your application.
However, at least concerning Node.js's process.nextTick function (which queues microtasks), there is an inbuilt protection against such blocking by means of process.maxTickDepth. This value is set to a default of 1000, cutting down further processing of microtasks after this limit is reached which allows the next macrotask to be processed)
So when to use what?
Basically, use microtasks when you need to do stuff asynchronously in a synchronous way (i.e. when you would say perform this (micro-)task in the most immediate future).
Otherwise, stick to macrotasks.
Examples
macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
microtasks: process.nextTick, Promises, queueMicrotask, MutationObserver
Basic concepts in spec:
An event loop has one or more task queues.(task queue is macrotask queue)
Each event loop has a microtask queue.
task queue = macrotask queue != microtask queue
a task may be pushed into macrotask queue,or microtask queue
when a task is pushed into a queue(micro/macro),we mean preparing work is finished,so the task can be executed now.
And the event loop process model is as follows:
when call stack is empty,do the steps-
select the oldest task(task A) in task queues
if task A is null(means task queues is empty),jump to step 6
set "currently running task" to "task A"
run "task A"(means run the callback function)
set "currently running task" to null,remove "task A"
perform microtask queue
(a).select the oldest task(task x) in microtask queue
(b).if task x is null(means microtask queues is empty),jump to step (g)
(c).set "currently running task" to "task x"
(d).run "task x"
(e).set "currently running task" to null,remove "task x"
(f).select next oldest task in microtask queue,jump to step(b)
(g).finish microtask queue;
jump to step 1.
a simplified process model is as follows:
run the oldest task in macrotask queue,then remove it.
run all available tasks in microtask queue,then remove them.
next round:run next task in macrotask queue(jump step 2)
something to remember:
when a task (in macrotask queue) is running,new events may be registered.So new tasks may be created.Below are two new created tasks:
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)
setTimeout(callback,n)'s callback is a task,and will be pushed into macrotask queue,even n is 0;
task in microtask queue will be run in the current round,while task in macrotask queue has to wait for next round of event loop.
we all know callback of "click","scroll","ajax","setTimeout"... are tasks,however we should also remember js codes as a whole in script tag is a task(a macrotask) too.
I think we can't discuss event loop in separation from the stack, so:
JS has three "stacks":
standard stack for all synchronous calls (one function calls another, etc)
microtask queue (or job queue or microtask stack) for all async operations with higher priority (process.nextTick, Promises, Object.observe, MutationObserver)
macrotask queue (or event queue, task queue, macrotask queue) for all async operations with lower priority (setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering)
|=======|
| macro |
| [...] |
| |
|=======|
| micro |
| [...] |
| |
|=======|
| stack |
| [...] |
| |
|=======|
And event loop works this way:
execute everything from bottom to top from the stack, and ONLY when the stack is empty, check what is going on in queues above
check micro stack and execute everything there (if required) with help of stack, one micro-task after another until the microtask queue is empty or don't require any execution and ONLY then check the macro stack
check macro stack and execute everything there (if required) with help of the stack
Micro stack won't be touched if the stack isn't empty. The macro stack won't be touched if the micro stack isn't empty OR does not require any execution.
To sum up: microtask queue is almost the same as macrotask queue but those tasks (process.nextTick, Promises, Object.observe, MutationObserver) have higher priority than macrotasks.
Micro is like macro but with higher priority.
Here you have "ultimate" code for understanding everything.
console.log('stack [1]');
setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);
const p = Promise.resolve();
for(let i = 0; i < 3; i++) p.then(() => {
setTimeout(() => {
console.log('stack [4]')
setTimeout(() => console.log("macro [5]"), 0);
p.then(() => console.log('micro [6]'));
}, 0);
console.log("stack [7]");
});
console.log("macro [8]");
/* Result:
stack [1]
macro [8]
stack [7], stack [7], stack [7]
macro [2]
macro [3]
stack [4]
micro [6]
stack [4]
micro [6]
stack [4]
micro [6]
macro [5], macro [5], macro [5]
--------------------
but in node in versions < 11 (older versions) you will get something different
stack [1]
macro [8]
stack [7], stack [7], stack [7]
macro [2]
macro [3]
stack [4], stack [4], stack [4]
micro [6], micro [6], micro [6]
macro [5], macro [5], macro [5]
more info: https://blog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3
*/
Macro tasks include keyboard events, mouse events, timer events (setTimeout) , network events, Html parsing, changing Urletc. A macro task represents some discrete and independent work. micro task queue has higher priority so macro task will wait for all the micro tasks are executed first.
Microtasks, are smaller tasks that update the application state and should be executed before the browser continues with other assignments such as
re-rendering the UI. Microtasks include promise callbacks and DOM mutation changes. Microtasks enable us to execute certain actions before the UI is re-rendered, thereby avoiding unnecessary UI rendering that might show an inconsistent application state.
Separation of macro and microtask enables the
event loop to prioritize types of tasks; for example, giving priority to performance-sensitive tasks.
In a single loop iteration, one macro task at most is processed
(others are left waiting in the queue), whereas all microtasks are processed.
Both task queues are placed outside the event loop, to indicate that the act of adding tasks to their matching queues happens outside the
event loop. Otherwise, any events that occur while JavaScript code is
being executed would be ignored. The acts of detecting and adding
tasks are done separately from the event loop.
Both types of tasks are executed one at a time. When a task starts executing, it’s executed to its completion. Only the browser can stop
the execution of a task; for example, if the task takes up too much
time or memory.
All microtasks should be executed before the next rendering because their goal is to update the application state before rendering occurs.
The browser usually tries to render the page 60 times per second,
It's accepted that 60 frames per second are the rate at which
animations will appear smooth. if we want to achieve smooth-running
applications, a single task, and all microtasks generated by that task
should ideally complete within 16 ms. If a task gets executed for more
than a couple of seconds, the browser shows an “Unresponsive script”
message.
reference John Resig-secrets of JS Ninja
I created an event loop pseudocode following the 4 concepts:
setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering are part of the macrotask queue. One macrotask item will be processed first.
process.nextTick, Promises, Object.observe, MutationObserver are part of the microtasks queue. The event loop will process all of the items in that queue including once that were processed during the current iteration.
There is another queue called animation queue which holds animation changes task items which will be processed next. All tasks which exists in this queue will be processed (not including new one which were added during the current iteration). It will be called if it is time for rendering
The rendering pipeline will try to render 60 times a second (every 16 ms)
while (true){
// 1. Get one macrotask (oldest) task item
task = macroTaskQueue.pop();
execute(task);
// 2. Go and execute microtasks while they have items in their queue (including those which were added during this iteration)
while (microtaskQueue.hasTasks()){
const microTask = microtaskQueue.pop();
execute(microTask);
}
// 3. If 16ms have elapsed since last time this condition was true
if (isPaintTime()){
// 4. Go and execute animationTasks while they have items in their queue (not including those which were added during this iteration)
const animationTasks = animationQueue.getTasks();
for (task in animationTasks){
execute(task);
}
repaint(); // render the page changes (via the render pipeline)
}
}

Categories