实验
先做一个实验:来看看立即解决的 Promise 和立即超时( 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) 是一个静态函数,可返回立即解决的 promise。 setTimeout(callback, 0)
执行延迟为 0 毫秒的回调。
打开执行并检查控制台。您会看到日志先打印了 'Resolved!'
,然后打印了 'Timeout completed!'
。立即解决的承诺比立即超时处理得更快。
是因为 Promise.resolve(true).then(...)
在 setTimeout(..., 0)
之前被调用了,所以 promise 的处理过程会更快吗?
那我们就修改一下实验条件,先调用 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 在立即超时之前已得到处理。所以。。。这是为什么?
事件循环
与异步 JavaScript 有关的问题可以通过探索事件循环解答答。先回顾一下异步 JavaScript 的工作原理。
调用栈(call stack) 是 LIFO(后进先出)的结构,用于存储在代码执行期间创建的执行上下文。简而言之,调用栈执行用来函数。
Web API 是异步操作(fetch 请求、promises、计时器),回调等待这里的工作完成。
任务队列(task queue)是一个 FIFO(先进先出)的结构,其中包含准备好执行的异步操作的回调。例如,超时的 setTimeout()
的回调(准备执行)进入任务队列中。
工作队列(job queue) 是一个 FIFO(先进先出)的结构,其中包含准备执行的 promise 的回调。例如,已解决的 resolve 或拒绝回调进入工作队列中。
最后,事件循环(event loop) 会一直监视调用栈是否为空。如果调用栈为空,则事件循环会查找工作队列或任务队列,并使准备好执行的回调出队到调用栈中。
工作队列与任务队列
下面从事件循环的角度来看一下前面的实验。我会逐步分析代码的执行情况。
- 调用堆栈执行
setTimeout(..., 0)
并“安排”一个计时器。timeout()
回调存储在 Web API 中:
setTimeout(function timeout() { console.log('Timed out!');}, 0);
Promise.resolve(1).then(function resolve() {
console.log('Resolved!');
});
- 调用栈执行
Promise.resolve(true).then(resolve)
并“安排”一个 promise 解析。resolved()
回调存储在 Web API 中:
setTimeout(function timeout() {
console.log('Timed out!');
}, 0);
Promise.resolve(1).then(function resolve() { console.log('Resolved!');});
- promise 立即解决,计时器立即超时。这时计时器回调
timeout()
被“排队”到任务队列,promise 回调resolve()
被“排队”到工作队列:
- 这里是最有意思部分:事件循环优先级使工作排在任务之上。事件循环使 promise 回调
resolve()
从工作队列中出队,并将其放入调用栈中,然后调用栈执行 promise 回调resolve()
:
setTimeout(function timeout() {
console.log('Timed out!');
}, 0);
Promise.resolve(1).then(function resolve() {
console.log('Resolved!');});
'Resolved!'
被输出到控制台。
- 最后,事件循环把计时器回调
timeout()
从任务队列中移出到调用栈中。然后调用栈执行计时器回调timeout()
:
setTimeout(function timeout() {
console.log('Timed out!');}, 0);
Promise.resolve(1).then(function resolve() {
console.log('Resolved!');
});
'Timed out!'
已输出到控制台。
此时调用栈为空。脚本执行已完成。
总结
为什么立即解决的 promise 比立即计时器处理得更快?
是由于事件循环的“优先级”使任务队列(存储已实现的 promise 的回调)中的任务从任务队列(存储超时的setTimeout()
回调)中的任务中出队。
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
欢迎继续阅读本专栏其它高赞文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 实现虚拟现实游戏
- 13个帮你提高开发效率的现代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切
- WebSocket实战:在 Node 和 React 之间进行实时通信
- 关于 Git 的 20 个面试题
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什么?
- 30分钟用Node.js构建一个API服务器
- Javascript的对象拷贝
- 程序员30岁前月薪达不到30K,该何去何从
- 14个最好的 JavaScript 数据可视化库
- 8 个给前端的顶级 VS Code 扩展插件
- Node.js 多线程完全指南
- 把HTML转成PDF的4个方案及实现
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。