JavaScriptのPromiseは、非同期処理が終わるまで、その処理の代理人としてがんばってくれるオブジェクトである。おかげで、非同期処理を同期処理のようにシンプルに書ける。
導入理由
JavaScriptにPromiseが導入されたのは、非同期処理をわかりやすく書きたい、という強いモチベーションがあったからだ。古き良きsetTimeout()のような関数(正確にはwindowオブジェクトのメソッド)であれば、コールバックによる非同期処理でもさしたる問題はなかった。
しかし、Node.jsランタイムの登場で非同期処理の活用シーンが爆発的に増加し、従来のコールバックによる非同期処理ではソースがえらく分かりづらくなった。
例えば以下のようなイメージである。
引用元:Callback Hell
いわゆるコールバック地獄と呼ばれるコードだ。
ネストは浅い方が見通しが良い。書くのも楽だ。先行言語にはすでにFutureあるいはPromiseと呼ばれる仕組みがある。それらを参考に直感的に書ける独自のPromise APIを実装するNode.jsライブラリなどが登場した。
これは標準にすべきだ、という潮流が生まれ、現在に至る。そんな感じだろう(あれこれ読んだ上での推測)。
作成方法
JavaScriptでは、newキーワードを使ってPromise()コンストラクタを呼び出すことでPromiseオブジェクトを作成する。引数には、resolve、rejectという2つの引数を取る関数を渡す。余談だがこの関数はexecutorと呼ばれる。実行したい非同期処理は、executorの中に書く。
resolveとrejectは、非同期処理が終わった場合に、処理を次に進めるための関数オブジェクトである。処理が成功した場合はresolve()関数にその値を渡し、失敗した場合はreject()関数にその理由(普通はErrorインスタンス)を渡して実行する。
上記のサンプルコードでは、1/2の確率でどちらかが実行される。
値は空でもいいが、嘘だった。TypeScriptの場合、型を指定せずに呼び出すと警告が出る。例えば空で呼び出す場合は以下のようにする。
resolve()あるいはreject()のいずれかを呼び出さないと、Promiseが抱える非同期処理は終わらない。果たされない約束として電子の海に留まることになる。なお、executorが返す値は無視される。可哀想。
使い方
Promiseインスタンスは、いわば非同期処理の代理人(Proxy)である。作っただけでは何も得られない。この代理人に、処理が終わったらどうするか指定しなければならない。
そのために、同インスタンスのthen()メソッド、あるいはawaitキーワードを使って、結果を得る処理を書く必要がある。
then()メソッド
Promiseインスタンスのthenメソッドは、2つの関数を引数に受け取る。1つ目はresolve()関数が実行された場合に実行されるもので、2つ目はreject()関数が実行された場合に実行される。
Promiseインスタンスのメソッドにはthenの他にcatch()とfinally()があるがここでは簡便のために割愛する。勘弁だ。どちらかというとエラーハンドリングの領域だし。
結局コールバックやないかい、と感じるか、わかりやすいと感じるかは、個人の感性によるところかと思う。
awaitキーワード
awaitキーワードを使うと、より同期処理に近い形で非同期処理を書くことができる。
このようにawaitをつけるだけで、非同期処理の代理人はデキる紳士のようになる。具体的には、resolve()関数に渡された結果をそのまま返してくれる。reject()が実行された場合は例外が投げられるので、なるべくならtry{}catch{}ブロックの中で実行した方が無難だ。
また、awaitキーワードはモジュールのトップレベルか、asyncキーワードをつけて定義された関数の中でしか使えない。加えて、awaitで待機できるのは、最も近いasyncキーワード関数の中だけである。
つまりasync関数の外側の処理は待機されない。注意したい。