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