Skip to content

Latest commit

 

History

History
154 lines (116 loc) · 6.13 KB

column-promise-resolve.adoc

File metadata and controls

154 lines (116 loc) · 6.13 KB

コラム: Promiseは常に非同期?

Promise.resolve(value) 等を使った場合、 promiseオブジェクトがすぐにresolveされるので、.then に登録した関数も同期的に処理が行われるように錯覚してしまいます。

しかし、実際には .then で登録した関数が呼ばれるのは、非同期となります。

const promise = new Promise((resolve) => {
    console.log("inner promise"); // 1
    resolve(42);
});
promise.then((value) => {
    console.log(value); // 3
});
console.log("outer promise"); // 2

上記のコードを実行すると以下の順に呼ばれていることが分かります。

inner promise // 1
outer promise // 2
42            // 3

JavaScriptは上から実行されていくため、まず最初に <1> が実行されますね。 そして次に resolve(42); が実行され、この promise オブジェクトはこの時点で 42 という値にFulfilledされます。

次に、promise.then<3> のコールバック関数を登録しますが、ここがこのコラムの焦点です。

promise.then を行う時点でpromiseオブジェクトの状態が決まっているため、 プログラム的には同期的にコールバック関数に 42 を渡して呼び出すことはできますね。

しかし、Promiseでは promise.then で登録する段階でpromiseの状態が決まっていても、 そこで登録したコールバック関数は非同期で呼び出される仕様になっています。

そのため、<2> が先に呼び出されて、最後に <3> のコールバック関数が呼ばれています。

なぜ、同期的に呼び出せるのにわざわざ非同期的に呼び出しているでしょうか?

同期と非同期の混在の問題

これはPromise以外でも適用できるため、もう少し一般的な問題として考えてみましょう。

この問題はコールバック関数を受け取る関数が、 状況によって同期処理になるのか非同期処理になるのかが変わってしまう問題と同じです。

次のような、コールバック関数を受け取り処理する onReady(fn) を見てみましょう。

mixed-onready.js
link:embed/embed-mixed-onready.js[role=include]

mixed-onready.jsではDOMが読み込み済みかどうかで、 コールバック関数が同期的か非同期的に呼び出されるのかが異なっています。

onReadyを呼ぶ前にDOMの読み込みが完了している

同期的にコールバック関数が呼ばれる

onReadyを呼ぶ前にDOMの読み込みが完了していない

DOMContentLoaded のイベントハンドラとしてコールバック関数を設定する

そのため、このコードは配置する場所によって、 コンソールに出てくるメッセージの順番が変わってしまいます。

この問題の対処法は、常に非同期で呼び出すように統一することです。

async-onready.js
link:embed/embed-async-onready.js[role=include]

この問題については、 Effective JavaScript項目67 非同期コールバックを同期的に呼び出してはいけない で紹介されています。

  • 非同期コールバックは(たとえデータが即座に利用できても)決して同期的に使ってはならない。

  • 非同期コールバックを同期的に呼び出すと、処理の期待されたシーケンスが乱され、 コードの実行順序に予期しない変動が生じるかもしれない。

  • 非同期コールバックを同期的に呼び出すと、スタックオーバーフローや例外処理の間違いが発生するかもしれない。

  • 非同期コールバックを次回に実行されるようスケジューリングするには、setTimeout のような非同期APIを使う。

— David Herman
Effective JavaScript

先ほどの promise.then も同様のケースであり、この同期と非同期処理の混在の問題が起きないようにするため、 Promiseは常に非同期 で処理されるということが仕様で定められているわけです。

最後に、この onReady をPromiseを使って定義すると以下のようになります。

onready-as-promise.js
link:embed/embed-onready-as-promise.js[role=include]

Promiseは常に非同期で実行されることが保証されているため、 setTimeout のような明示的に非同期処理にするためのコードが不要となることが分かります。