Promise とは
Promise とは JavaScript で非同期実行をきれいに記述するための仕様です。この仕様の簡潔な記述を「Promises/A+」で見ることができます。本物の仕様は「25.4 Promise Objects | ECMAScript® 2015 Language Specification」にありますが、読んでぱっとわかるような代物ではありません。
JavaScript における非同期実行とその結果の処理
非同期実行で実行される処理は、いつ完了するかわからない処理です。例えば、HTTP で通信して URL が指す内容を取得する処理 fetch(url) があったとします。
response = fetch('https://example.com/api/me')
console.log(response.status)
もし fetch() が__同期__実行と仮定すると、fetch() の呼び出しは HTTP 通信が完了するまで返ってきません。しかし JavaScript は基本的にシングルスレッドで、上のスクリプトが完了するまで(非常に長い時間がかかる可能性があります)他のスクリプトを実行できません。
それでは困ってしまうので fetch() を__非同期__に実行したい、つまり fetch() の呼び出しは即座に返ってきて処理の本体はバックグラウンドで__並行__して実行してほしい、 と考えたくなるのは当然でしょう。
上のコードで fetch() が HTTP 通信の完了を待たずに返ってくるとすると、通信が完了した場合に得られる結果を処理するコード console.log(response.status) を fetch() 呼び出しの後ろに直書きすることはできません。この時点では response.status がまだ得られていないからです。
一つの解決策は、結果を処理するコードを__関数__として fetch() に渡すことです。これは旧来から JavaScript で提供されていた手法です。仮に fetch() がこの手法で実装されていたとすると、以下のようになるでしょう。
fetch('https://example.com/api/me',
(response) => console.log(response.status))
この手法の難点は、結果を処理するコード中でさらに非同期処理を呼び出したとき(非同期処理がネストしたとき)にコードが読みにくくなる点にあります。いわゆるコールバック地獄と呼ばれるものです。
response からダウンロードした JSON を表示するコードは、例えば以下のような読みにくいコードになってしまうでしょう。
fetch('https://example.com/api/me',
(response) => response.json(
(data) => fetch(`https://example.com/api/user/${data.user_id}`,
(response) => response.json(
(data) => console.log(data)))))
Promise による解法
実際には fetch() と response.json() は Promise に準拠していますので、コードは以下のようになります。
fetch('https://example.com/api/me')
.then((response) => response.json())
.then((data) => fetch(`https://example.com/api/user/${data.user_id}`))
.then((response) => response.json())
.then((data) => console.log(data))
パイプライン処理っぽくなって読みやすくなりました。
Promise が提供する手法の要点は、結果処理関数を非同期実行関数呼び出しの__後__に設定できるようにした点です。非同期実行関数と結果処理関数の記述を__分離__できるようにしたわけです。
p = fetch('https://example.com/api/me')
p.then((response) => console.log(response.status))
非同期実行関数呼び出しの後に結果処理関数を設定できるようにするために、fetch() が Promise インスタンス p を返すようになっています。この Promise インスタンス p が非同期処理を管理している、と言うことができるでしょう。このPromise インスタンス p に対して p.then() メソッドで p が管理する非同期処理の結果処理関数を設定できます。
p が管理する非同期処理(この場合は HTTP 通信処理)が完了し、結果である response が得られた時点で p.then() の引数である結果処理関数が response を引数として実行されます。
Promise 結果処理関数の実行タイミング
前節の終わりで「非同期処理が完了した時点で結果処理関数が実行されます」と書きました。実際には__いつ__実行されるのでしょうか?答えは、
- 非同期実行処理を記述したスクリプトが終了した__後__で実行される
となります。
p = fetch('https://example.com/api/me')
p.then((response) => console.log(response.status))
/* ここにすごくすごく長い処理が書いてある */
例えば上のようなスクリプトの場合、fetch() により起動された非同期処理の HTTP 通信が並行して実行され短時間で完了したとしても、終了処理関数 (response) => console.log(response.status) は「すごくすごく長い処理」が実行された後で実行されます。
注意してほしいのは、これは__実装ではなく仕様__だということです。
もう一つ注意してほしい点があります。以下のコードで考えて見ましょう。
p = fetch('https://example.com/api/me')
/* ここにすごくすごく長い処理1が書いてある */
p.then((response) => console.log(response.status))
/* ここにすごくすごく長い処理2が書いてある */
このコードで fetch() により起動された非同期処理が短時間で完了したと仮定しましょう。この場合 p.then((response) => console.log(response.status)) の呼び出し時に (response) => console.log(response.status) を実行するのには何の問題もありません(その時点で response が得られているので)。しかし実際に実行されるのは「すごくすごく長い処理2」が実行された後になります。
これも__実装ではなく仕様__です。
Promise によるネストした非同期処理の記述
この節ではネストした非同期処理がパイプライン的にきれいに記述できる理由を見ていきます。その要点は、p.then() の呼び出しもまた Promise インスタンスを返すことにあります。
ただし、この返された Promise インスタンスは p ではありません。メソッド呼び出しをずらずら続けて書くことができる「フルーエント」なメソッドは this を返すことで実現されていることが多いですが、.then() メソッドは違います。
p = fetch('https://example.com/api/me')
q = p.then((response) => response.json())
q.then((data) => console.log(data))
q は p ではなく、p.then() の内部で生成された新しい Promise インスタンスです。この q は p.then() に渡された関数 (response) => response.json() を__非同期処理として管理__する Promise インスタンスとして作成されます。つまり (response) => response.json() の実行が完了したら、その結果を引数として q.then() で設定した結果処理関数が実行されます。
(response) => response.json() は p.then() によって fetch() の結果処理関数として設定された関数ですから、最終的には fetch() → (response) => response.json() → (data) => console.log(data) という非同期実行のドミノ倒しが設置されたことになります。
このような仕組みによりネストした非同期処理の連鎖をパイプライン的に記述できるようになっているのです。上のコードから p, q への代入を消去すると以下のようなフルーエントなコードになります。
fetch('https://example.com/api/me')
.then((response) => response.json())
.then((data) => console.log(data))
Promise インスタンスを返す結果処理関数
実は前節では少し嘘を書きました。response.json() は JSON をデコードした data を返すのではなく、Promise インスタンスを返す API です1。
コードを再掲します。
p = fetch('https://example.com/api/me')
q = p.then((response) => response.json())
q.then((data) => console.log(data))
response.json() が Promise インスタンスを返すので、今までの説明を素直に解釈すると q.then() で設定した結果処理関数 (data) => console.log(data) には引数として Promise インスタンスが渡されることになってしまって、これではまずいです。
非同期処理が完了して、.then() で設定した結果処理関数を実行する Promise の内部実装では以下のような場合分けが行われています(あくまでも説明のための疑似コードです)。
r = response.json() /* p の結果処理関数 */
if (/* r が Promise インスタンスでない */)
console.log(r) /* q の結果処理関数 */
else
r.then((data) => console.log(data)) /* q の結果処理関数 */
p の結果処理関数 (response) => response.json() が Promise インスタンス r を返したとします。この場合、p.then((response) => response.json()) が返した Promise インスタンス q に対して q.then() で設定した関数 (data) => console.log(data) はすぐには実行されません(実行できません)。その代わり、その関数は r.then() によって r が管理する非同期処理が完了したときの結果処理関数として設定されるのです2。これによって (data) => console.log(data) は r が管理する非同期処理が完了するまで実行が遅延されます。
別の言い方でまとめてみます。
q = p.then((a) => r)
によって p → q という Promise インスタンスの連鎖が形成されます。p が管理する非同期処理が完了すると p.then() で設定された処理 (a) => r が実行されます。r が Promise インスタンスでないなら即座に、Promise インスタンスならそれが管理する非同期処理の完了時に q.then() で設定される処理が実行されます3。
Promise について知っておくべきその他のこと
-
p.then()による結果処理関数の設定は複数行うこともできますし、pが管理する非同期処理が完了した後でも行うことができます。また、p.then()で設定した結果処理関数は高々1度しか実行されません。このように Promise における結果処理関数の挙動にはこの記事では説明していない細かい仕様がいくつも存在します。 - この記事では正常に完了した場合の処理の記述について__のみ__説明しましたが、Promise では異常終了(例外発生)した場合の処理関数についても記述できます(
.catch()メソッド)。正常処理の場合を理解していれば簡単に理解できると思います。 - 複数の非同期処理、つまり複数の Promise インスタンスを扱う API が存在します。複数の非同期処理の全部の完了を待ったり(
Promise.all())、最初の終了を待ったり(Promise.race())する処理を記述できます。 - 従来のコールバックによる非同期処理 API(例えば
setTimeout())を Promise に準拠するようにラップできます。これにはnew Promise()を使います。
これらについて知りたい場合、以下のページがわかりやすいと思います :
-
正確に言うと、
p.then()の引数の結果処理関数(response) => response.json()が実行されるときには HTTP 通信は完全には完了しておらず、HTTP レスポンスのヘッダーを読み終わったタイミングです。response.json()は、HTTP レスポンスのボディ部分のダウンロードという非同期実行を表す Promise インスタンスを返すのです。 ↩ -
q.then()によって結果処理関数が設定される__前__にこの処理を行う場合もあり得るので、実際の Promise の実装はかなり違ったことを行っています。 ↩ -
rが管理する非同期処理が再び Promise インスタンスを返す事態も考えられます。この場合はq.then()で設定される処理はさらにその Promise インスタンスが管理する非同期処理の完了まで実行が遅延されます。これは非同期処理が Promise インスタンス以外のものを返すまで無限に続きます。 ↩