JavaScript执行环境一,Promise里的代码为什么比setTimeout先执行

首先,如果我们是浏览器或者 Node 的开发者,我们该如何使用 JavaScript 引擎。
当拿到一段 JavaScript 代码的时候,浏览器或者 Node 环境首先要做的就是;传递给 JavaScript 引擎,并且要求它去执行;然而,执行 JavaScript 并非一锤子买卖,宿主环境当遇到一些事件时,会继续将一段代码传递给 JavaScript 引擎去执行,此外,我们可能还会提供 API 给 JavaScript 引擎;比如 setTimeout 这样的 API,它会允许 JavaScript 在特定的时机执行。
所以,我们应该形成一个感性的认知:一个 JavaScript 引擎会常驻于内存中,它等待着我们(宿主)把 JavaScript 代码或者函数传递给它执行。
在 ES3 和更早的版本中,JavaScript 本身没有执行异步的能力,这也就意味着宿主传递给 JavaScript 引擎一段代码,引擎就直接顺序的把它执行了,这个任务也就是宿主发起的任务。
但是在 ES5 之后,JavaScript 引入了 Promise;这样,不需要宿主环境的那排,JavaScript 引擎本身也可以发起任务了;通过 JSC 引擎的术语,我们把宿主发起的任务称为宏观任务,把 JavaScript 发起的任务称为微观任务。

宏观任务和微观任务

JavaScript 引擎等待宿主环境分配宏观任务,而通常这样一个等待的行为都是一个事件循环,在 Node 中,也会把这个部分称为事件循环。
整个事件循环做的事情基本上就是反复的“等待-执行”,这里每次的执行过程其实都是一个宏观任务,大致理解为宏观任务的队列就相当于事件循环。
在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这个异步代码在一个宏观任务中执行,因此,每个宏观任务中又包含了一个微观任务队列;有了宏观任务和微观任务机制,我们就可以实现 JavaScript 引擎级和宿主级的任务了;例如:Promise 永远在队列尾部添加微观任务,setTimeout 等宿主 API 则会添加宏观任务。

Promise

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

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

Promise 的 then 回调是一个异步执行的过程,下面学习一下异步执行的过程,如下示例:

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

执行后,这里的输出结果会打印 a,b,c;在进入 console.log(‘b’) 之前,r 已经得到了 resolve,但是 Promise 的 resolve 始终是异步操作,所以 c 无法出现在 b 之前。

接下来看看 setTimeout 和 Promise 的混用;示例如下:

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

这里设置两个互不相关的异步操作,通过 setTimeout 打印 d,通过 Promise 打印 c;不论代码顺序如何,这里始终是 c 先打印 d 后打印;因为 Promise 产生的微观任务,而 setTimeout 产生的是宏观任务。
为了更清楚的说明微观任务先于宏观任务执行,看下面一个强制 1s 后执行的代码:

1
2
3
4
5
6
7
8
9
10
11
12
setTimeout(()=>console.log("d"), 0)
var r1 = new Promise(function(resolve, reject){
resolve()
});
r1.then(() => {
var begin = Date.now();
while(Date.now() - begin < 1000);
console.log("c1")
new Promise(function(resolve, reject){
resolve()
}).then(() => console.log("c2"))
});

这里强制 1s 的耗时,保证了 c2 任务在 d 之后被添加到任务队列;这里的顺序是 c1,c2,d,可以看出即使耗时 1s 的 c1 执行完毕再执行 c2,任然先于 d 执行,这就解释刚刚说明的问题。
最后,分析总结一下异步执行的顺序:

  • 首先分析有几个宏观任务
  • 每个宏观任务中又包含了几个微观任务
  • 根据调用次序,确定宏观任务中微观任务的执行顺序
  • 再根据宏观任务的触发规则和调用次序,确定宏观任务的执行次序
  • 最终确定整个执行顺序

再看一个稍微复杂的例子,如下:

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"));

这里 setTimeout 把代码分割成了两个宏观任务,这里不管5s还是0s都一样。
第一个宏观任务中包含了先后执行的 console.log(‘a’) 和 console.log(‘b’);setTimeout 后,第二个宏观任务执行调用了 resolve,然后 then 中的代码异步得到执行,所以调用了 console.log(‘c’),最终输出的顺序是:a、b、c

Promise 是 JavaScript 中的一个定义,但是实际编码时,它并不比回调简单多少;庆幸的是从 ES6 开始,我们有了 async/awati,这个语法改进跟 Promise 配合能够更有效的改善代码的可读性。

async/await

async/await 是在 ES2016 中新加入的特性,它的运行时基础是 Promise,看一下用法;async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数;但 async 函数是一种特殊语法,特征是在 function 前面加上关键字 async,这样就定义了一个 async 函数,这样我们就可以在其中使用 await 来等待一个 Promise。

1
2
3
4
5
6
7
8
9
10
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 中使用 await 调用了 Promise 函数。除此之外,async 函数还可以嵌套,可以利用 async 函数组合出新的 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");
}

这里的 foo2 中利用 await 调用了两次 foo 函数,形成了嵌套。

此外,generatot/iterator 也常常和异步一起讲,其实 generator/iterator 并非异步代码,只是在缺少 async/await 的时候,一些框架(如框架 co)使用它们的特性来模拟 async/await。

总结

这里重新认识了 JavaScript 的宏观任务和微观任务。由宿主环境发起的任务是宏观任务,由 JavaScript 引擎发起的任务是微观任务;而许多的微观任务队列又组成了宏观任务。

generato/iterator 还要再了解一下。。