I bring to your attention a translation of the article “Tasks, microtasks, queues and schedules” by Jake Archibald (Jake Achibald), who is in the position of Developer Advocate for Google Chrome.When I told my colleague Matt Gant that I was thinking about writing an article about the sequence of microtasks and the order of their execution within the browser’s event cycle, he said, "Jake, I will be honest, I will not read about this." Well, I nevertheless wrote, so sit back and let’s see it together, okay?
In fact, if it will be easier for you to watch the video, there is a
wonderful speech by Philip Roberts at JSConf, which talks about the event cycle - it does not cover microtasks, but otherwise it is a great introduction to the topic. In any case, they drove ...
')
Let's look at the following javascript code:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
What do you think, in what order should the logs be displayed?
The correct answer is:
script start
,
script end
,
promise1
,
promise2
and
setTimeout
, however, while the order in different browsers is quite often different.
Microsoft Edge, Firefox 40, iOS Safari and Desktop Safari 8.0.8 log
setTimeout
before
promise1
and
promise2
. What is really strange is that Firefox 39 and Safari 8.0.7 worked correctly.
Why it happens
For a more accurate understanding of the process, you must first imagine how the event loop processes tasks and microtasks. For the first time, this may seem too complicated. Deep breath…
Each “stream” has its own event loop, and therefore every web worker, so that they can run independently, while all windows from the same domain (according to the same origin rule) share the same event loop, because they can Communicate synchronously with each other. The event cycle runs continuously, executing queued tasks. Tasks are performed sequentially and cannot intersect. Okay, okay, don't leave ...
Tasks are planned in such a way that the browser can step out of their wilds to the ground of JavaScript / DOM and be sure that these actions take place alternately. Handling a mouse click event callback requires task scheduling, as well as parsing HTML and
setTimeout
from the example above.
setTimeout
waits for a specified delay and then plans a new task for its callback. Therefore,
setTimeout
is output to the log after
script end
, since
script end
logging is part of the first task, and the output of the word
setTimeout
is the second. Have patience, we are almost at the goal, ahead of the most interesting ...
Microtasks are usually planned for things that should be executed immediately after the current executable script. For example, responding to a stack of actions or to do something asynchronously without having to lose performance from scratch because of a completely new task. The microtask queue unfolds at the end of each complete task, as well as after callbacks in the event that no other JavaScript is at the execution stage. Any additional microtasks that are queued during the deployment of a microtask queue are added to the end of the queue and are also processed. Microtasks include mutation observer callouts and promises, as in the example above.
As soon as the promise is solved or if it has already been resolved, he queues the microtask for the execution of the kolbek. This makes sure that promise callbacks are executed asynchronously, even if they are already solved. So, the
.then(func)
call for a resolved promise immediately queues a microtask. That is why
promise1
and
promise2
are output to the log after the
script end
, because the current executable script must complete before microtasks are processed.
promise1
and
promise2
are logged before
setTimeout
for microtasks are always deployed to the next big task.
Note translator: in this place, the author originally inserted a great visual presentation of the work of the JavaScript scheduler , but I hardly have the technical opportunity to repeat this on Habré, for this I am sending an inquisitive reader to the original page.Yes, I really did a step-by-step animated diagram. How did you spend your Saturday, probably walking somewhere outdoors with friends? Well, I - no. In case something is not clear in my awesome UI, try clicking the arrows left and right.
What is wrong with some browsers?
They log
script start
,
script end
,
setTimeout
,
promise1
and
promise2
. Promise calls are executed after these
setTimeout
. It seems that for the promise callbacks there is a whole separate task instead of a simple microsadget. This behavior can lead to performance problems when using promises, because Kolbek can unfairly be postponed until rendering and other things related to a large task. Here are applications for correcting anomalies in
Edge and
Firefox (note of the translator: by the time the translation was written in the application for Firefox, it turned out that only the 40th and 41th versions suffer from unexpected behavior, and since the 42nd anomaly is not reproduced) . The nightly builds of WebKit are behaving as expected, so I guess that soon Safari will return to the righteous path again.
How to understand when tasks are used, and when micro tasks are used.
Although this is how we make the assumption that the implementation is correct, the only way is to test. See order of log output regarding promises and
setTimeout
.
The exact way is to see the specification. For example,
step 14 setTimeout
task, whereas
in the mutation fix specification step 5 creates a microtask.
In the ECMAScript world, microtasks are called jobs. In
step 8.a of the PerformPromiseThen specification for queuing microtasks, EnqueueJob is
EnqueueJob
. Unfortunately, for the time being there is no explicit relationship between tasks (“jobs”) and microtasks, however,
in one of the es-discuss mailings it was mentioned that they should use a common queue.
Now let's take a look at a more comprehensive example.
In the hall, someone will cry out in confusion, “No, they are not ready!”. Do not pay attention, you are ready.
Level One: Fight with the Boss
The following problem might seem complicated to me before I wrote this post. Here is a small piece of HTML:
<div class="outer"> <div class="inner"></div> </div>
Logically, what will the next javascript code log if I click the
div.inner
?
Try to think before you go to the answer. Hint: logs can be displayed more times.
Test
Note translator: the author in this place in the blog has an interactive DOM element (direct link) where you can personally check the behavior of your browser.You thought it would be different? I hasten to reassure you, perhaps you were right. Unfortunately, different browsers have different degrees of acceptance of this opinion:
 |  |  |  |
---|
- click
- promise
- mutate
- click
- promise
- mutate
- timeout
- timeout
| - click
- mutate
- click
- mutate
- timeout
- promise
- promise
- timeout
| - click
- mutate
- click
- mutate
- promise
- promise
- timeout
- timeout
| - click
- click
- mutate
- timeout
- promise
- timeout
- promise
|
Who is right?
Handling a click event is a task. Kolki Mutation observer and promise are queued as microtasks. Kolbek
setTimeout
is a task.
(Note of the translator: here again an interactive diagram explaining step by step the principle of operation of the code given earlier, I recommend to take a look.)So Chrome behaves correctly. For me, the news was to find out that microtasks are deployed after callbacks (unless this is part of the execution of another JavaScript script), I thought that their deployment is limited only by the end of the task. This rule is described in the HTML specification for calling callbacks:
Perform a microtask checkpoint
- HTML: Cleaning up after a callback , step 3
... and checkpoint microtasks means nothing more than deploying a queue of microtasks, unless we are already deploying a queue of microtasks. And this is what the ECMAScript job specification tells us (“jobs”):
There is no running for you.
- ECMAScript: Jobs and Job Queues
... although "can be" in the context of HTML has the character of "must be", i.e. "must".
What did the browsers misunderstand?
Firefox and
Safari rightly empty the queue of microtasks between click handlers, as can be seen from the callback mutations, but promises are queued differently. This could be forgiven, especially given the nebula of communication between the task (“jobs”) and the microtask, but I expected them to be executed between the processors.
Application for Firefox. Application for Safari.We already understood that Edge did not place the promises in the queue correctly, but he also did not empty the microtasks queue between click handlers, instead, the queue turned around only after calling all the handlers, which explains the only output
mutate
after both
click
in the magazine.
This is mistake.Boss's Evil Brother from Level One
Pancake! And what if to add to the previous example:
inner.click();
The event will begin to be processed in the same way as before, but by calling from the script, and not from the actual user interaction.
Test
Note translator: in the original there is another interactive platform where you can click a button and find out the correct answer for your browser (direct link). |  |  |  |
---|
- click
- click
- promise
- mutate
- promise
- timeout
- timeout
| - click
- click
- mutate
- timeout
- promise
- promise
- timeout
| - click
- click
- mutate
- promise
- promise
- timeout
- timeout
| - click
- click
- mutate
- timeout
- promise
- timeout
- promise
|
And I do not cease to receive various results in Chrome, I have already updated this table a hundred times thinking that I had previously mistakenly checked in Canary. If you have other results in Chrome, tell me in the comments on which version you are.
Why is it different now?
Note translator: in this place, one more last time, the author gives us the opportunity to enjoy the visualization of the wonders of browser builders' engineering ideas (link, again, direct).So the correct order is the following:
click
,
click
,
promise
,
mutate
,
promise
,
timeout
and the last
timeout
, which seems to mean that Chrome is working correctly.
After each of the click handlers is called ...
Perform a microtask checkpoint
- HTML: Cleaning up after a callback , step 3
Previously, this meant that microtasks would be executed between click handlers, however explicit
.click()
happens synchronously, so the script that caused
.click()
between click handlers will still be on the stack. This rule ensures that microtasks do not interrupt the execution of JavaScript code. This means that the microtask queue will not be deployed until all the handlers are executed; the queue to microtasks comes only after all event handlers.
Is it important?
Still, it will eat you from the inside (phew). I ran into this when I tried to create a
concise wrapper over IndexedDB , using promises instead of terrible IDBRequest objects. With her, the
IDB almost became pleasant to me .
When a success event is triggered in the IDB,
the transaction object becomes inactive after transferring control (step 4). If I create a promise that is solved during the initiation of this event, the handlers should execute before step 4 while the transaction is still active, but this does not happen in any browser other than Chrome, because of which the library becomes useless as it were.
In Firefox, you can cope with this, because polymes of promises, such as
es6-promise , use Mutation observers for callbacks that are nothing more than microtasks. Safari with this fix enters a race condition, but the matter is most likely in their
broken implementation of the IDB . Unfortunately, IE / Edge is currently not subject to correction, as mutation events do not occur after callbacks.
It remains only to hope that in this matter we will sometime be able to observe interchangeability.
We did it!
Finally:
- Tasks are executed in order and the browser can render in the intervals between them.
- Microtasks are executed in order and executed:
- after each callback, unless it is part of executing some other script
- at the end of each task
I hope after all that I’ve read, it’s easier to think in terms of the event loop; at least, there was an extra reason to relax.
Has anyone stayed here? Alia ?! Hello?