setTimeout与Promise的执行先后顺序

之前只是知道setTimeout和Promise都会使后续程序进入异步执行,但是其实这里面的差别还是很大的,Google了下,发现一篇关于此的讲解,发现非常有用,总结记录一下

setTimeout与Promise的执行先后顺序
Photo by Markus Spiske / Unsplash

之前只是知道setTimeout和Promise都会使后续程序进入异步执行,但是其实这里面的差别还是很大的,Google了下,发现一篇关于此的讲解,发现非常有用,总结记录一下

一、代码尝试

我们先来尝试执行下,究竟是setTimeout的回调先执行还是Promise的?

setTimeout(function timeout() {
  console.log('Timed out!');
}, 0);
Promise.resolve(1).then(function resolve() {
  console.log('Resolved!');
});
// logs 'Resolved!'
// logs 'Timed out!'

Promise.resolve(1) 是一个静态函数,返回一个立即被执行的Promise。setTimeout(callback, 0) 会在0毫秒后被执行。

执行上面的代码你会发现控制台输出了'Resolved!',然后是'Timeout completed!'。显然,一个立即resolved的Promise是要快于一个立即执行的setTimeout的。

因为Promise.resolve(true).then(...)在setTimeout(..., 0) 之前调用导致的这个结果吗?我们接着看。

调换一下两句代码的执行顺序

setTimeout(function timeout() {
  console.log('Timed out!');
}, 0);
Promise.resolve(1).then(function resolve() {
  console.log('Resolved!');
});
// logs 'Resolved!'
// logs 'Timed out!'

看下Console输出,结果并没有因此改变。

这些代码演示了, 一个立即resolved的Promise确实优先于一个立即执行的setTimeout(..., 0)执行,但这是为什么呢?

二、Event Loop

有关于Javascript的异步相关的问题,我们可以先初步探究下Event Loop。然我们回顾下Javascript用来实现异步功能组件 Event Loop。

可以看到,Node.js的调用栈是一个LIFO(后进先出)队列,这个队列中保存了代码执行过程中创建的执行上下文。用栈中的函数调用会依次出栈执行。

Web APIs 指异步函数返回后将要被调用的回调函数,这些异步回调函数由 fetch,requests,promises,timers 等产生。

图中 task queue 也称为宏任务(macrotasks) 是一个 FIFO (先进先出) 队列,这个队列中是将要被执行的异步回调。比如说,执行setTimeout() 便会将此回调入栈。

图中 job queue 也称为微任务(microtasks)同样是一个FIFO(先进先出)队列,但是此队列中会保存Promise的resolve或reject的回调,将会在Promise被resolved或者rejected后入栈。

Event Loop 会检查调用栈是否为空。如果调用栈空了,则查看 job queue 或者 task queue,若这两个队列中存在回调,则执行出栈,进入调用栈执行回调。

3. Job queue vs task queue

然我们从 Event Loop 的视角,重新审视一下上面的代码,一步一步来分析一下。

A) 执行 setTimeout(..., 0) ,将回调函数放入 Web APIs:

B) 执行Promise.resolve(true).then(resolve) 将回调函数放入 Web APIs:

C) Promise 被立即resolved,timeout也立即结束。两个callback分别进入 task queue 和 job queue:

D) 关键部分来了:在Event Loop中,job queue的出栈要优先于task queue,也就是说,会优先执行微任务(microtask)。因此,会先从 job queue 中出栈,Promise的回调被放入到调用栈中执行:

E) 最后,Event Loop 从 task queue中出栈 timeout 回调,回调函数被压入调用栈,接着调用栈中timeout()函数被执行:

调用栈为空,代码执行结束。

4. 结语

现在清楚了为什么一个立即结束的Promise会总是先于立即结束的timer执行了。

因为 Event Loop 会优先执行 job queue 中的微任务(microtasks),然后执行 task queue 中的宏任务(macrotasks)。

Promise产生的是微任务,setTimeout 产生的则是宏任务。

That's it!

参考:https://dmitripavlutin.com/javascript-promises-settimeout/