为什么 Promise 比 setTimeout 处理得快

测试

首先,让我们做一个测试。哪一个处理得更快:一个立即完成的 promise 还是一个立即执行的 setTimeout(又称为 0 毫秒过期时间)?

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

静态函数 Promise.resolve(1) (opens new window) 返回一个立即完成的 promise。setTimeout(callback, 0) 在延时 0 毫秒后执行回调函数。

可以打开 demo (opens new window) 查看输出结果。你将看到 'Resolved!' 首先被打印,然后才是 Timeout completed!。所以一个立即完成的 promise 比立即执行的 setTimeout 进度更快。

promise 进度更快是因为 Promise.resolve(true).then(...) 先于 setTimeout(..., 0) 被执行?合理的怀疑。

让我们考虑调整测试,将 setTimeout(..., 0) 放在前面执行:

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

嗯哼,得到相同的结果!

setTimeout(..., 0) 先于 Promise.resolve(true).then(...) 被执行。然而,打印结果 'Resolved!' 依然在 'Timed out!' 之前。

该测试展示了立即完成 promise 先于 SetTimeout 完成执行。那么重要的问题是为什么?

事件循环 EVENT-LOOP

这个问题关联 JavaScript 异步事件循环来回答。让我们回忆 JavaScript 异步怎样运行。

注意:如果你不熟悉事件循环,在阅读接下来的内容之前我推荐先观看视频 (opens new window)

事件循环

call stack 调用栈是一种 LIFO (Last In, First Out) 的结构,存储着代码调用执行上下文。简单来说,调用堆执行函数。

Web APIs 是异步操作(fetch 请求,promise,时间器)与它们的回调函数等待完成。

task queue(任务队列)是一种 FIFO (First In, First Out) 的结构,存储着准备执行的异步操作的回调函数。举例来说,setTimeout() 的回调函数 -- 准备执行 -- 在任务队列中排队。

job queue(工作队列)是一种 FIFO (First In, First Out) 的结构,存储着准备执行的 promise。举例来说,完整 promise 的完成或拒绝的回调函数在任务队列中排队。

最后,event loop 事件循环永久地监视调用栈是否为空。如果调用栈是空的,事件循环会查看工作队列或任务队列,并将任何准备执行的回调去掉到调用栈中。

工作队列与任务队列

让我们从事件循环思维思考上面的测试。我将一步一步地分析代码调用。

A) 调用栈执行 setTimeout(..., 0) 并 schedule (记录)一个定时器 timer。timeout() 回调函数存储在 Web APIs 中:

 
 
 





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

事件循环 setTimeout

B) 调用栈执行 Promise.resolve(true).then(resolve) 并 schedule (记录)一个 promise 结果。resolved() 回调函数存储在 Web APIs 中:





 
 
 

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

事件循环 promise

C) promise 立即被解析,同时定时器也立即被定时。因此,定时器回调 timeout()enqueued 添加到任务队列,promise 回调 resolve()enqueued 添加到工作队列:

事件循环 enqueued

D) 现在是有趣的部分:事件循环调用工作队列优先于任务队列。事件循环将 promise 回调 resolve() 从工作队列调到调用栈中去。然后调用栈执行 promise 回调resolve()






 


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

'Resolved!' 打印在控制台。

事件循环 job queue

E) 最后,事件循环将定时器回调 timeout() 从任务队列调到调用栈中。然后调用栈执行定时器回调 timeout()


 






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

'Timed out!' 打印在控制台。

事件循环 task queue

现在调用栈空白。脚本执行已然完成。

总结

为什么立即完成的 promise 比立即定时器处理得快?

因为事件循环优先从工作队列(存储已完成的 promise 的回调)中去排队,而不是从任务队列(存储定时器 setTimeout() 回调)中去排队。