2021年12月12日

マイクロタスク

Promiseハンドラ.then/.catch/.finallyは常に非同期です。

Promiseがすぐに解決された場合でも、.then/.catch/.finallyの行のコードは、これらのハンドラよりも先に実行されます。

デモはこちら

let promise = Promise.resolve();

promise.then(() => alert("promise done!"));

alert("code finished"); // this alert shows first

実行すると、最初にcode finishedが表示され、次にpromise done!が表示されます。

これは奇妙です。なぜなら、Promiseは最初から確実に完了しているからです。

なぜ.thenが後でトリガーされたのでしょうか?何が起きているのでしょうか?

マイクロタスクキュー

非同期タスクには適切な管理が必要です。そのため、ECMA標準では内部キューPromiseJobsを指定しており、これは多くの場合「マイクロタスクキュー」(V8用語) と呼ばれています。

仕様に記載されているとおりです。

  • キューは先入れ先出しです。最初にエンキューされたタスクが最初に実行されます。
  • タスクの実行は、他に何も実行されていない場合にのみ開始されます。

より簡単に言えば、Promiseの準備が整うと、その.then/catch/finallyハンドラはキューに入れられます。まだ実行されません。JavaScriptエンジンが現在のコードから解放されると、キューからタスクを取り出して実行します。

そのため、上記の例では「code finished」が最初に表示されます。

Promiseハンドラは常にこの内部キューを経由します。

複数の.then/catch/finallyを持つチェーンがある場合、それぞれが非同期的に実行されます。つまり、最初にキューに入れられ、現在のコードが完了し、以前にキューに入れられたハンドラが完了したときに実行されます。

順番が重要な場合はどうすればよいでしょうか?code finishedpromise doneの後に出力するにはどうすればよいでしょうか?

簡単です。.thenでキューに入れてください。

Promise.resolve()
  .then(() => alert("promise done!"))
  .then(() => alert("code finished"));

これで、意図した順序になります。

未処理の拒否

記事Promiseでのエラー処理unhandledrejectionイベントを覚えていますか?

これで、JavaScriptが未処理の拒否を発見する方法を正確に確認できます。

「未処理の拒否」は、マイクロタスクキューの最後でPromiseエラーが処理されなかった場合に発生します。

通常、エラーを予期している場合は、.catchをPromiseチェーンに追加して処理します。

let promise = Promise.reject(new Error("Promise Failed!"));
promise.catch(err => alert('caught'));

// doesn't run: error handled
window.addEventListener('unhandledrejection', event => alert(event.reason));

しかし、.catchの追加を忘れると、マイクロタスクキューが空になった後、エンジンがイベントをトリガーします。

let promise = Promise.reject(new Error("Promise Failed!"));

// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

後でエラーを処理する場合はどうでしょうか?このように。

let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')), 1000);

// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

実行すると、最初にPromise Failed!が表示され、次にcaughtが表示されます。

マイクロタスクキューについて知らなければ、「なぜunhandledrejectionハンドラが実行されたのでしょうか?エラーをキャッチして処理しました!」と不思議に思うかもしれません。

しかし、今では、unhandledrejectionはマイクロタスクキューが完了したときに生成されることがわかります。エンジンはPromiseを調べ、いずれかのPromiseが「拒否済み」状態にある場合、イベントがトリガーされます。

上記の例では、setTimeoutによって追加された.catchもトリガーされます。しかし、それはunhandledrejectionが発生した後に行われるため、何も変わりません。

まとめ

Promise処理は常に非同期です。すべてのPromiseアクションは、内部の「Promiseジョブ」キュー(「マイクロタスクキュー」(V8用語) とも呼ばれる)を通過するためです。

そのため、.then/catch/finallyハンドラは常に現在のコードが完了した後に呼び出されます。

.then/catch/finallyの後でコードを実行することを保証する必要がある場合は、チェーンされた.then呼び出しに追加できます。

ブラウザやNode.jsを含むほとんどのJavaScriptエンジンでは、マイクロタスクの概念は「イベントループ」と「マクロタスク」と密接に関連しています。これらはPromiseと直接関係がないため、チュートリアルの別の部分、イベントループ:マイクロタスクとマクロタスクの記事で説明されています。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点の提案があれば、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数単語のコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。