为什么 Promise 比 setTimeout 处理得快
测试
首先,让我们做一个测试。哪一个处理得更快:一个立即完成的 promise 还是一个立即执行的 setTimeout(又称为 0 毫秒过期时间)?
Promise.resolve(1).then(() => {
console.log('Resolved!')
})
setTimeout(() => {
console.log('Timed out!')
}, 0)
// logs 'Resolved!'
// logs 'Timed out!'
静态函数 Promise.resolve(1) 返回一个立即完成的 promise。setTimeout(callback, 0) 在延时 0 毫秒后执行回调函数。
可以打开 demo 查看输出结果。你将看到 'Resolved!' 首先被打印,然后才是 Timeout completed!。所以一个立即完成的 promise 比立即执行的 setTimeout 进度更快。
promise 进度更快是因为 Promise.resolve(true).then(...) 先于 setTimeout(..., 0) 被执行?合理的怀疑。
让我们考虑调整测试,将 setTimeout(..., 0) 放在前面执行:
setTimeout(() => {
console.log('Timed out!')
}, 0)
Promise.resolve(1).then(() => {
console.log('Resolved!')
})
// logs 'Resolved!'
// logs 'Timed out!'
嗯哼,得到相同的结果!
setTimeout(..., 0) 先于 Promise.resolve(true).then(...) 被执行。然而,打印结果 'Resolved!' 依然在 'Timed out!' 之前。
该测试展示了立即完成 promise 先于 SetTimeout 完成执行。那么重要的问题是为什么?
事件循环 EVENT-LOOP
这个问题关联 JavaScript 异步事件循环来回答。让我们回忆 JavaScript 异步怎样运行。
注意:如果你不熟悉事件循环,在阅读接下来的内容之前我推荐先观看视频

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(() => {
console.log('Timed out!')
}, 0)
Promise.resolve(1).then(() => {
console.log('Resolved!')
})

B) 调用栈执行 Promise.resolve(true).then(resolve) 并 schedule (记录)一个 promise 结果。resolved() 回调函数存储在 Web APIs 中:
setTimeout(() => {
console.log('Timed out!')
}, 0)
Promise.resolve(1).then(() => {
console.log('Resolved!')
})

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

D) 现在是有趣的部分:事件循环调用工作队列优先于任务队列。事件循环将 promise 回调 resolve() 从工作队列调到调用栈中去。然后调用栈执行 promise 回调 resolve():
setTimeout(() => {
console.log('Timed out!')
}, 0)
Promise.resolve(1).then(() => {
console.log('Resolved!')
})
'Resolved!' 打印在控制台。

E) 最后,事件循环将定时器回调 timeout() 从任务队列调到调用栈中。然后调用栈执行定时器回调 timeout():
setTimeout(() => {
console.log('Timed out!')
}, 0)
Promise.resolve(1).then(() => {
console.log('Resolved!')
})
'Timed out!' 打印在控制台。

现在调用栈空白。脚本执行已然完成。
总结
为什么立即完成的 promise 比立即定时器处理得快?
因为事件循环优先从工作队列(存储已完成的 promise 的回调)中去排队,而不是从任务队列(存储定时器 setTimeout() 回调)中去排队。
本文由 吳文俊 翻译,原文地址 Why Promises Are Faster Than setTimeout()?