This is a translation of an article by Jake Archidald. The original intention of translation is that WHEN I looked up macro task and micro task, I did not find authoritative Chinese documents about these two concepts, and the Chinese materials I could find were mostly technical blogs. The authoritative article I could find so far was written by Jake Archidald, an engineer of Google. However, my English ability and technical ability are limited, it is quite difficult to read the original text directly, so I want to translate the article for the convenience of my friends who are not good at English. You are welcome to point out any inappropriate points in translation, or give some suggestions and criticisms. Thank you, ღ(´ ᴗ · ‘).

The original demo had a cool dynamic step demo, and I drew static images instead. (* ̄)  ̄) → Task-microtasks-queues -and schedules! (* ̄) → Task-microtasks-queues -schedules!

Tasks, microtasks, queues and scheduling

When I told my colleague Matt Gaunt that I was going to write an article about microtask queues and their execution in event loops in browsers, he said, honestly, Jake, I’m not going to read that. Okay, I’ll do it anyway. So now let’s sit back and enjoy it, shall we?

In fact, if you prefer video, Philip Roberts has a great talk at JSconf on the topic of Event Loop, and while it doesn’t mention microtasks, it does provide a great introduction to other parts. Anyway, let’s move on

Take a look at the JavaScript snippet below:

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');
Copy the code

What is the correct print order?

The answer is:

script start
script end
promise1
promise2
setTimeout
Copy the code

However, the order of printing varies depending on the browser support.

Microsoft Edge, Firefox 40, iOS Safari, and Safari 8.0.8 print setTimeout first, then promise1 and promise2 although this seems to be a race condition (see note below). This is really weird, because under Firefox 39 and Safari8.0.7 it always works.

Why is that?

To understand this, you need to understand how event loops handle macro and micro tasks. When you first encounter these concepts, they can be daunting. Take a deep breath ~

Every thread has its own event loop, and every Web worker(JavaScript running in the background) is no exception. So it can be executed independently. All Windows in the same browser share the same event cycle because they can communicate synchronously. The event loop is continuous and executes tasks in the task queue. There are multiple task sources in an event loop, ensuring that tasks from the same source can be executed in sequence (for example, IndexedDB defines their own specifications). Within each event loop, the browser selects the source to perform the task. Browsers first choose to perform performance-sensitive tasks, such as processing user input. All right, all right, let’s move on…

Tasks are executed in a predetermined order, so the browser internally ensures that js code and DOM manipulation are executed in an orderly fashion. Browsers may render and update views between tasks. Callbacks from mouse-click events go to the task queue, as does HTML parsing. The setTimeout mentioned in the above example is no exception.

The setTimeout callback is queued as a new task after the given time. That’s why you print script End first and then setTimeout. Because printing script End is part of the first task, printing setTimeout belongs to another task. Well, that’s pretty much all we need to know, but I hope you’ll be patient and read the rest…

Microtasks are usually executed immediately after the execution of the current script, for example, in response to actions, or asynchronous actions that are not part of a new task. The callback function in the microtask queue is called as soon as the current JavaScript code completes, or as soon as each task completes. If the microtask queue has any other new microtasks while waiting, this microtask is also inserted to the end of the queue. Microtasks include the callback function of MutationObserver, as well as the promise mentioned above.

Once the state of a Promise instance changes, or after the state of the instance changes, its callback function goes into the microtask queue. This ensures that the callback function is asynchronous when the promise state changes. So then the callback in.then() goes into the microtask queue. This is why promise1 and promise2 print after script end. Because the current script does not process the microtasks until it has finished executing. Print promisE1 and promise2 before setTimeout, because microtasks are always performed first before the next task.

So, step by step:

Yes, I created a dynamic step diagram. How was your Saturday? Did you go out in the sun with your friends? Well, I didn’t. If my drawing is not clear, you can click on the arrow and go step by step.

Why do some browsers behave inconsistently?

Some browsers print in the following order:

script start
script end
setTimeout
promise1
promise2
Copy the code

They do setTimeout first and then promise. It’s possible that they treat the Promise callback as a new task rather than a microtask.

This makes sense because promises come from ECMAScript, not HTML. In ECMAScript, there is a similar concept of “jobs”, but the relationship between them is not clear in vague mailing list discussions. However, there is a general consensus that promises are part of the microtask queue, and for good reason.

Classifying promises as tasks raises some performance issues. That’s because promise’s callback will delay things related to the macro task, such as rendering the interface. This also creates uncertainty due to the source of the task and may break connections to other apis. More on that later.

Edge treats promise as a microtask, and WebKit keeps doing the right thing, so I think Safari will eventually fix this, and it looks like Firefox43 has.

Interestingly enough, both Safari and Firefox tried using promises as tasks and later fixed the problem. I wonder if it’s a coincidence.

How do you identify tasks and microtasks?

One way is testing. Take a look at the time nodes that Promise & setTimeout prints, though we’ll ultimately have to make that judgment based on how it’s executed.

A better way to be sure is to consult the documentation. For example, setTimeout is a task and mutation Record is a microtask.

As mentioned above, tasks are called “jobs” in ECMAScript. In step 8. In a of PerformPromiseThen, EnqueueJob is a microtask.

Now, let’s look at a more complicated example. There may be some newbies who say they’re not ready. Ignore them

Level one

Before writing this article, I could be wrong. Here’s a little HTML snippet:

<div class="outer">
  <div class="inner"></div>
</div>
Copy the code

The corresponding JS fragment looks like this. What is output if you click div.inner?

// Let's get hold of those elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the // outer element new MutationObserver(function() { console.log('mutate'); }).observe(outer, { attributes: true }); // Here's a click listener... function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } / /... which we'll attach to both elements inner.addEventListener('click', onClick); outer.addEventListener('click', onClick);Copy the code

Let’s go ahead and try it out before we look at the answer. Tip: Print more than one message.

Do your answers agree with the ones above? If not, you’re probably right. Performance is not consistent across browsers

So who’s right?

The Mutation Observer and promise are microtasks, and the setTimeout callback is also a macro task, so the code execution process looks like this:

So Chrome’s output is correct. One thing THAT surprised me was that the microtask was executed after the clicked callback function, given that the JavaScript code was not currently performing the task. I thought it would be terminated. This rule comes from the HTML specification for calling callback functions:

If the call stack of the current script is empty, the microtask queue is called.

— HTML: Cleaning up after a callback step 3

A microtask checkpoint involves traversing the microtask queue, unless we are already working on the microtask queue. Roughly, ECMAScript says this about Jobs:

Execution of a task can only be started when the execution context stack that is not running is empty

— ECMAScript: Jobs and Job Queues

Although “might” becomes “must” in an HTML context.

So what’s wrong with these browsers?

Firefox and Safari correctly called the microtask queue between two click event listeners, as shown by the mutation callback function, but the Promise did not appear to be on the microtask queue. This is forgivable, given that the connection between Jobs and MicroTask is vague. But I still want them to execute between listening callbacks, and Firefox does, and Safari does.

In Edge, we’ve seen that it doesn’t line up promise’s callbacks correctly. But it also doesn’t call the microtask queue correctly between the two click event listeners, but only after all the click callbacks have been called, which explains why two clicks are printed before mutate is printed. This is a bug.

Level 2

Ohh, brother, is still the above example. What happens if we just execute the following statement?

inner.click();
Copy the code

As above, a click event will be sent, but it will be triggered by the script rather than the actual interaction.

I swear I will always get different results when running chrome, I have updated this chart so many times that I even suspect my code is wrong. If you get different results in Chrome, please let me know the Chrome version in the comments.

Why is it different?

So the correct order is: click, click, Promise, mutate, promise, timeout, timeout, and Chrome seems to behave correctly.

After each call to the event listener callback…

If the call stack of the current script is empty, the microtask queue is called.

— HTML: Cleaning up after a callback step 3

In the previous example, this meant that the microtask was running between two click-event callbacks, but.click would cause events to be dispatched synchronously, so the script that called.click() would still be in the call stack between the two callbacks. The above rules ensure that microtasks do not interrupt executing JavaScript. This means that we do not process the queue of microtasks between callbacks, but after two click listens.

Does any of this matter?

Yes, it can come back to haunt you unconsciously. This happened to me when I tried to create a simple wrapper library for IndexedDB. The library uses Promises instead of the odd IDBRequest object. It makes IDB use interesting.

When the IDB triggers a success event, the associated transaction object becomes inactive after time dispatch (Step 4). If I create an Resolved Promise object when this event is triggered, the callback should run before Step 4 because the transaction will still be active, but this will not happen outside of Chrome, This would make the IndexedDB library a bit of a cipher.

In fact, you can fix this problem in Firefox, because Promise polyfill(such as ES6-Promise) uses mutation observers as callbacks that will correctly use microtasks, Safari seems to be affected by Race Conditions, but this could just be their disruption of the IDB implementation. Unfortunately, IE/Edge always fails because the mutation event is not processed after the callback function is executed.

Hopefully we’ll see some interoperability here soon.

You’re done!

To sum up:

  • Tasks are executed sequentially, and browsers may render views between different tasks
  • Microtasks are also arranged in order, when executing time:
    • After each callback (assuming the JS stack is empty)
    • At the end of each task

Hopefully, you now know how to handle the event loop, or at least have an excuse to lie down.

Is there anyone else there? hello? hello?

The original post is on my GitHub: Portal


Wikipedia defines a race condition as follows:

Race hazard, also known as race condition, describes how the output of a system or process depends on an uncontrolled sequence or timing of events. The word comes from two signals trying to compete with each other to influence who comes out first.

For example, if two processes in a computer attempt to modify the contents of a shared memory at the same time, without concurrency control, the final result depends on the order and timing of the execution of the two processes. And if a concurrent access conflict occurs, the final result is incorrect.