为什么 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()?