​ 当拿到一段 JavaScript 代码时,浏览器或者 Node 环境首先要做的就是;传递给 JavaScript 引擎,并且要求它去执行。然而,执行 JavaScript 并非一锤子买卖,宿主环境当遇到一些事件时,会继续把一段代码传递给 JavaScript 引擎去执行,此外,我们可能还会提供 API 给 JavaScript 引擎,比如 setTimeout 这样的 API,它会允许 JavaScript 在特定的时机执行。所以,我们首先应该形成一个感性的认知:一个 JavaScript 引擎会常驻于内存中,它等待着我们(宿主)把 JavaScript 代码或者函数传递给它执行。

一、JavaScript 的执行

​ 在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,这也就意味着,宿主环境传递给 JavaScript 引擎一段代码,引擎就把代码直接顺次执行了,这个任务也就是宿主发起的任务。

​ 但是,在 ES5 之后,JavaScript 引入了 Promise,这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了。

​ JavaScript 语言,我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。

二、宏观任务与微观任务

​ JavaScript 引擎等待宿主环境分配宏观任务,在操作系统中,通常等待的行为都是一个事件循环,所以在 Node 术语中,也会把这个部分称为事件循环。

宏观任务的队列就相当于事件循环

​ 在宏观任务中,JavaScript 的 promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此每个宏观任务中又包含一个微观任务队列。

MacroTask

​ microTask microTask microTask

MacroTask

​ microTask microTask microTask

MacroTask

​ 有了宏观任务和微观任务机制,我们就可以实现 JavaScript 引擎级和宿主级的任务了,例如:Promise 永远在队列尾部添加微观任务。setTimeout 等宿主 API,则会添加宏观任务。

三、Promise

​ Promise 是 JavaScript 语言提供的一种标准化的异步管理方式。promise 总体思想是,需要进行 IO、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(promise 的 then 方法回调)

1
2
3
4
5
6
function sleep(duration) {
return new Promise((resolve, reject) => {
setTimeout(resolve, duration);
});
}
sleep(1000).then(() => console.log("finished"));

sleep 函数的作用:等候传入参数指定的时长。

Promise 的 then 回调是一个异步的执行过程。示例代码:

1
2
3
4
5
6
var test = new Promise(function (resolve, reject) {
console.log("a");
resolve();
});
test.then(() => console.log("c"));
console.log("b");

打印顺序:a > b > c

​ 在进入 console.log(‘b’)之前,毫无疑问 test 已经得到了 resolve,但是 Promise 的 resolve 始终是异步操作,所以 c 无法出现在 b 之前。

当 setTimeout 和 Promise 混用,设置两段互不相干的异步操作:通过 setTimeout 执行 console.log(‘d’),通过 Promise 执行 console.log(‘c’)

1
2
3
4
5
6
7
var test = new Promise(function (resolve, reject) {
console.log("a");
resolve();
});
setTimeout(() => console.log("d"), 0);
test.then(() => console.log("c"));
console.log("b");

我们发现,不论代码顺序如何,d 必定发生在 c 之后,因为 Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 API,它产生宏任务。

理解微任务始终先于宏任务,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 执行一个耗时1秒的Promise
setTimeout(() => console.log("d"), 0);
var r = new Promise(function (resolve, reject) {
resolve();
});
r.then(() => {
var begin = Date.now();
while (Date.now() - begin < 1000);
console.log("c1");
new Promise(function (resolve, reject) {
resolve();
}).then(() => console.log("c2"));
});

执行顺序:c1 > c2 > d 。 即使耗时 1 秒的 c1 执行完毕,再 enque 的 c2,仍然先于 d 执行,这可以很好的解释了微任务优先的原理。

异步执行的顺序:

1、分析有多少个宏任务

2、在每个宏任务中,分析有多少个微任务

3、根据调用次序,确定宏任务中的微任务执行次序

4、根据宏任务的触发规则和调用次序,确定宏任务的执行次序

5、确定整个顺序

复杂的例子:

1
2
3
4
5
6
7
8
function sleep(duration) {
return new Promise(function (resolve, reject) {
console.log("b");
setTimeout(resolve, duration);
});
}
console.log("a");
sleep(5000).then(() => console.log("c"));

执行顺序:a > b > c 。setTimeout 把整个代码分割成了 2 个宏观任务,,不论是 5 秒还是 0 秒,都是一样。

第一个宏观任务中,包含了先后同步执行的 console.log(‘a’)和 console.log(‘b’)

setTimeout 后,第二个宏观任务执行调用了 resolve,然后 then 中的代码异步得到执行,所以调用了 console.log(‘c’),最终输出的顺序为: a b c 。

​ Promise 是 JavaScript 中的一个定义,但是实际编写代码时,我们可以发现,它似乎并不比回调的方式书写更简单,但是从 ES6 开始,我们有了 async/await,这个语法改进跟 Promise 配合,能够有效地改善代码结构。

四、async/await

​ async/await 是 ES2016 新加入的特性,它提供了用 for、if 等结构来编写异步的方式。

它的运行时基础是 Promise。

​ async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数。

async 函数是一种特殊语法,特征是在 function 关键字之前加上一个 async 关键字,这样就定义了一个 async 函数,我们可以在其中使用 await 来等待一个 Promise。

1
2
3
4
5
6
7
8
9
10
11
function sleep(duration) {
return new Promise(function (resolve, reject) {
setTimeout(resolve, duration);
});
}
async function foo() {
console.log("a");
await sleep(2000);
console.log("b");
}
foo();

​ async 函数强大之处在于,它是可以嵌套的。

1
2
3
4
5
6
7
8
9
10
11
12
13
function sleep(duration) {
return new Promise(function (resolve, reject) {
setTimeout(resolve, duration);
});
}
async function foo(name) {
await sleep(2000);
console.log(name);
}
async function foo2() {
await foo("a");
await foo("b");
}