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 finished
をpromise 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と直接関係がないため、チュートリアルの別の部分、イベントループ:マイクロタスクとマクロタスクの記事で説明されています。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen…)を使用してください。