Promise 什么时候会被召唤?
承诺将在什么时候被调用?
在使用过程中,我之前没有太意识到,但我担心使用async/await会阻塞处理,所以我想知道Promise的回调将在什么时候进行,所以现在我对此进行了调查。
关于Promise被调用的机制,需要先了解事件循环和微任务。
如果先写结论的话,以下是大致的感觉。
-
- PromiseはEventLoop内のmicrotaskキューでFIFO実行される。
-
- Timer系の処理(setImmediateやsetTimeout)はmicrotaskが全て実行された後に実行される。(つまり、setTimeout(fn, 0)はmicrotaskを全て実行した後にfnを実行するという意味)
-
- async/awaitはPromiseの箇所でsuspendしているに過ぎない(イベントループをブロッキングするかどうかはPromise内部の処理に依存する)、ジェネレータ文法(yield)やコルーチンの概念と同じ。
-
- NodeJS v12以降ではasync/awaitによるパフォーマンスの劣化は改善されているので、処理の実行順序の見やすさ的に積極的に書いて問題ない。(ただし、async/await関係なしに待たなくて良い処理に関してはレスポンスを返した後に実行すべしなのとI/O系のSyncメソッドは使わないほうが良い)
- ブラウザでもPromiseはqueueMicrotaskによって実装されている(そもそもPromise、async/awaitサポートされてないブラウザもまだ生き残っているのでトランスパイル必須だが)
如果是NodeJS的情况
NodeJS是一个运行在V8引擎上的JavaScript执行环境(在Google Chrome的Chromium中也使用)。最初它是为了解决C10K问题(即在服务器的硬件性能足够的情况下,当客户端的并发连接数增加时,服务的响应变慢)而开发的后端环境(应用服务器)。C10K问题并不是由硬件性能引起的,而是由操作系统限制导致的客户端并发连接数的上限。
-
- プロセス数の上限
-
- コンテキストスイッチ(切り替え)のコスト
- ファイルディスクリプタの上限
为了解决这些问题,NodeJS的设计是采用单进程,单线程来处理请求。(实际上,它也可以创建多个进程和多个线程,但根本的设计理念是如此。)
通过单进程,单线程处理所有请求,可以避免受到进程数量上限的限制,并且不会产生大量的多进程、多线程的上下文切换。
数据库连接也不是每个多进程单独连接,而是在单线程内复用,这样就不会达到文件描述符的上限。
然而,由于在单线程中进行文件的读写会导致阻塞其他处理,因此我们支持和推荐异步I/O。
用于管理单线程处理的请求的机制是事件队列(事件循环)。
与传统的每个请求启动一个进程的多进程类型的应用服务器不同,
单进程、单线程会将请求逐个放入事件队列中,并在异步获取来自数据库和文件的数据后返回响应。

参考资料:现在才问的Node.js
以上所述,NodeJS解决了C10K问题,采用了单进程单线程的事件循环。然而,单线程带来的问题是,如果出现高负载的循环处理等情况,会阻塞事件循环,对所有请求产生延迟或最差响应的影响。
好吧,现在我们来讨论一下在NodeJS中Promise被调用的时机。
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();
执行结果如下:
同步执行→nextTick→Promise(微任务)→setTimeout→setImmediate
的顺序是这样的。
5
3
4
1
2
只有 (() => console.log(5))() 是同步任务,其他都是异步任务。
虽然我们可以理解同步任务是最先执行的,但是我们需要深入了解事件循环来看看异步任务的顺序是如何确定的。
事件循环是由 libuv 实现的,在 NodeJS 启动时初始化以下事件循环。
(但 nextTickQueue 和 microTaskQueue 是由 NodeJS 实现的)

首先,在进入事件循环之前或每个事件循环阶段之后,如果队列中存在任务,则会执行这些任务直到队列为空。
(由于事件循环是单线程的,无法同时处理多个任务)
-
- nextTickQueueは全てのキューの中で最速に処理される→nextTickが実行される
- microTaskQueueはnextTickQueueが空になり次第、実行(Promisesオブジェクトのコールバックはここに所属)→Promiseが実行される
当队列被消除后,会从计时器阶段进入事件循环。
-
- TimerフェーズでsetTimeoutが呼ばれる
- CheckフェーズでsetImmediateが必ず呼ばれる
因此同步執行 -> nextTick -> Promise(微任務)-> setTimeout -> setImmediate。
参考文献:关于Node.js中事件循环机制和定时器的细节
作为单线程的一个问题,我们已经提到了阻塞其他处理的问题。
即使使用async函数,如下例子所示,由于其底层仍然是单线程,执行高负载处理将会阻塞所有请求。
app.get('/compute-async', async function computeAsync(req, res) {
log('computing async!');
const hash = crypto.createHash('sha256');
const asyncUpdate = async () => hash.update(randomString());
for (let i = 0; i < 10e6; i++) {
await asyncUpdate();
}
res.send(hash.digest('hex') + '\n');
});
其实通过在setTimeout中间插入可以在不阻塞其他请求的情况下继续进行高负载处理。如果之前理解了事件循环,就可以通过在Promise await(微任务)之间插入setTimeout来将大量处理都压缩到微任务中并执行,而不是一次性执行所有处理后再执行。这样可以在微任务–>setTimeout–>微任务–>setTimeout之间加入间隔,避免了阻塞其他处理的情况发生。(这只是基于事件循环规范的一种应对措施,所以在NodeJS上直接进行过重的CPU处理并不适合。)
app.get('/compute-with-set-timeout', async function computeWSetTimeout (req, res) {
log('computing async with setTimeout!');
function setTimeoutPromise(delay) {
return new Promise((resolve) => {
setTimeout(() => resolve(), delay);
});
}
const hash = crypto.createHash('sha256');
for (let i = 0; i < 10e6; i++) {
hash.update(randomString());
await setTimeoutPromise(0);
}
log('done ' + req.url);
res.send(hash.digest('hex') + '\n');
});
参考: Node.js: 即使是快速的异步函数也可能阻塞事件循环,导致 I/O 饥饿。
在NodeJS中,存在着一个过程可能成为瓶颈,从而降低整个服务器性能的危险。另外,由于持续在同一进程中执行,一旦发生内存泄漏,服务器将无法继续运行,因此需要使用调试工具和测量工具来调查问题出在哪里。
参考:从零开始的Node.js性能调优
嗯,我已经知道Promise将在什么时候被执行了。(将在事件循环之间的微任务队列中被执行)

如果使用async/await进行执行,会发生什么情况呢?
使用async/await的函数会被转换为以下这种形式的函数,在V8引擎上执行。
被挂起,并在microtask队列中执行以返回挂起的返回值,
最终将implicit_promise作为Promise的结果返回。

在使用 async/await 的情况下,应该关注的另一个问题是性能下降。但是在 NodeJS v12 之后,使用原生的 Promise 几乎没有什么速度问题,所以我认为可以放心地积极使用 async/await,不用担心性能。

换句话说,async/await本身并不会导致性能下降或者事件循环被阻塞。(取决于Promise.then的具体实现方式可能会有这个问题)
参考:更快的异步函数和promises
如果是使用浏览器的情况下
在浏览器中,JavaScript的执行流程也是基于EventLoop,类似于Node.js。Promise和MutationObserver会在微任务中执行。
setTimeout(() => console.log("0"));
Promise.resolve()
.then(() => console.log("1"));
console.log("2");
在NodeJS中,执行顺序与之前相同,依次为同步处理→Promise(微任务)→setTimeout(计时器)。
2
1
0
在处理DOM事件和渲染时,另一个重要的点是所有的微任务在其他事件处理、渲染或计时器相关处理之前都会完成。(也就是说,在处理事件的同时,不会发生Promise网络处理导致数据被更改的情况)

参考: 事件循环(event loop): 微任务(microtask)和宏任务(macrotask)
可能性1:
也许Jake先生实现的那篇介绍了Chrome的WebWorker的博客更易于理解,因为它有样例和可执行的示例。
补充说明一下,我确认,解决了浏览器之间的差异,FireFox、Safari和Edge现在都与Chrome的行为一致(截至2020年6月21日)。
由于原文是2015年的,可能在旧版本的浏览器上存在差异。
参考:任务、微任务、队列和计划
順便提一下,如果你理解了以上内容,你就可以解决下一篇文章的高级问题了。
参考:你能答对多少题?关于 Promise 的13道题【附解析】