We know that js is single-threaded, so how does js handle asynchronous code? For example, the following code is how the output is performed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
console.log(1);
setTimeout(function() {
    console.log(2);
}, 0);
new Promise(function(resolve) {
    console.log(3);
    resolve(Date.now());
}).then(function() {
    console.log(4);
});
console.log(5);
setTimeout(function() {
    new Promise(function(resolve) {
        console.log(6);
        resolve(Date.now());
    }).then(function() {
        console.log(7);
    });
}, 0);

You can guess the final output without running it and then expand on what we are going to say.

1. macrotask and microtask

Based on our years of experience in writing ajax: js should be executed in the order of statements, and in the case of asynchronous, the asynchronous request is initiated and then executed down the line, and then executed again when the asynchronous result is returned. But how does he manage these execution tasks internally?

In js, tasks are divided into macrotask and microtask, which maintain a queue of tasks that are executed using a first-in-first-out policy! Tasks that are executed synchronously are executed on macrotasks.

The main macro tasks are: script (overall code), setTimeout, setInterval, I/O, UI interaction events, postMessage, MessageChannel, setImmediate (Node.js environment).

The main microtasks are: Promise.then, MutationObserver, process.nextTick (Node.js environment).

The specific operation steps are as follows.

  1. take a task from the head of a macro task and execute it.
  2. adding a micro-task to the queue of micro-tasks if it is encountered during execution.
  3. after the macro task is executed, whether there are tasks in the queue of micro-tasks, and if so, go out and execute them one by one until they are executed.
  4. the GUI renders.
  5. return to step 1 until the macro task is executed.

These 4 steps constitute a circular detection mechanism for events, which we call eventloop.

Returning to the code we mentioned above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
console.log(1);
setTimeout(function() {
    console.log(2);
}, 0);
new Promise(function(resolve) {
    console.log(3);
    resolve(Date.now());
}).then(function() {
    console.log(4);
});
console.log(5);
setTimeout(function() {
    new Promise(function(resolve) {
        console.log(6);
        resolve(Date.now());
    }).then(function() {
        console.log(7);
    });
}, 0);

The execution steps are as follows.

  1. execute log(1), outputting 1.
  2. encounter setTimeout and add the code log(2) of the callback to the macro task waiting to be executed.
  3. execute console.log(3), adding log(4) from then to the microtask.
  4. executing log(5), outputting 5.
  5. encounter setTimeout and add the code log(6, 7) from the callback to the macro task.
  6. when one task of the macro task is executed, check if a task exists in the micro task queue, a micro task log(4) exists (added in step 3), execute output 4.
  7. take the next macro task log(2) and execute it, output 2.
  8. one task of the macro task is executed, check if a task exists in the micro-task queue and it does not exist.
  9. fetch the next macro task to execute, execute log(6) and add log(7) from then to the microtask.
  10. the macro task is executed and a micro-task log(7) exists (added in step 9), executing output 7.

Thus, the final output order is: 1, 3, 5, 4, 2, 6, 7;

We implement a slightly more time-consuming operation in Promise.then, and this step will look more obvious.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
console.log(1);
var start = Date.now();
setTimeout(function() {
    console.log(2);
}, 0);
setTimeout(function() {
    console.log(4, Date.now() - start);
}, 400);
Promise.resolve().then(function() {
    var sum = function(a, b) {
        return Number(a) + Number(b);
    }
    var res = [];
    for(var i=0; i<5000000; i++) {
        var a = Math.floor(Math.random()*100);
        var b = Math.floor(Math.random()*200);
        res.push(sum(a, b));
    }
    res = res.sort();
    console.log(3);
})

In Promise.then, Mr. then becomes an array of 5 million random numbers, and then sorts this array. Running this code shows that: 1 will be output immediately, 3 will be output after a little while, and then 2. No matter how long we wait for 3 to be output, 2 will definitely be output after 3. This confirms the 3rd step in eventloop, which must wait for all microtasks to be executed before starting the next macro task.

Meanwhile, the output of this code is interesting.

1
2
3
setTimeout(function() {
    console.log(4, Date.now() - start); // 4, 1380 电脑状态的不同,输出的时间差也不一样
}, 400);

Originally to set is 400ms after the output, but because the previous task is seriously time-consuming, resulting in the task after the delay can only be scheduled backwards. Can also show that the delay of such operations as setTimeout and setInterval is inaccurate, these two methods can only be approximately 400ms after the task in the macro task, but the specific execution time, or to see whether the thread is idle. If there is a time-consuming operation in the previous task, or when an infinite number of micro-tasks are added, the execution of the next task will be blocked .

2. async-await

You can see from the code above that the code in Promise.then is a microservice, so how is the async-await code executed? For example, the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function A() {
    return Promise.resolve(Date.now());
}
async function B() {
    console.log(Math.random());
    let now = await A();
    console.log(now);
}
console.log(1);
B();
console.log(2);

In fact, async-await is just a syntactic sugar for Promise+generator. Let’s rewrite the above code like this for a little more clarity.

1
2
3
4
5
6
7
8
9
function B() {
    console.log(Math.random());
    A().then(function(now) {
        console.log(now);
    })
}
console.log(1);
B();
console.log(2);

This way we can understand the order of the output: 1, 0.4793526730678652(random number), 2, 1557830834679(timestamp);

3. requestAnimationFrame

requestAnimationFrame also belongs to methods whose execution is asynchronous, but I task the method is neither a macro task nor a micro task. As defined in MDN.

window.requestAnimationFrame() tells the browser - you want an animation to be executed and asks the browser to call the specified callback function to update the animation before the next redraw. This method requires a callback function to be passed as an argument, which will be executed before the browser’s next redraw.

requestAnimationFrame is executed before the GUI renders, but after the microservice, although requestAnimationFrame does not necessarily have to be executed at the current frame, it is up to the browser to decide which frame to execute at based on the current policy.

4. Summary

We have to remember the two most important points: js is single-threaded and the eventloop loop mechanism.