JavaScriptにおける非同期処理は一種の悪夢です。非同期処理は容易にコードを複雑化させ、品質の低下を招きます。そこでこの問題を解決するため、非同期処理を簡単に扱うことができる、Promiseasync/awaitという機能が導入されました。この記事では、Promiseasync/awaitを用いた非同期コードの単純化について簡単な解説をします。

Promise

実行順序がコード通りにはならない非同期処理

非同期処理とは何でしょうか。非同期な処理は、コードの順番通りには実行されません。どういうことか、簡単な例を見てみましょう。

このコードでは500ミリ秒後に「hello」と表示し、その後に「world」を表示しようとしています。ですが、実際には「world」の後に「hello」が表示されます。

1行目のコードは確かに「hello」を表示することを命令していますが、あくまで500ミリ秒後のリクエストでしかありません。リクエストを終えた直後に2行目に移動し、実際には「world」が先に表示され、500ミリ秒が経過してから「hello」が表示されます。

これが非同期処理です。非同期処理では、実行順序はコード通りにはなりません。

コールバック関数で非同期処理の後に処理を行う

非同期処理では実行順序がコード通りにはならないとは言っても、実際には順序が重要な場合があります。最も代表的な例はXHR(XMLHttpRequest)です。XHRを利用すれば、例えば外部ファイルを読み込むことができますが、基本的には非同期処理になります。

ファイルの読み込みでは、「ファイルを読み込み終わった後にxxxする」という処理をしたいことが多いはずです。ですが、非同期処理となると、そのままコードを書いただけではなかなかうまく行きません。つまり、以下のコードは正常に動作しません。

このコードを実行すると、実際にはファイルの読み込みが終わる前にconsole.logが実行されてしまいます。非同期処理に関する実行順序を制御するには、何らかの工夫が必要になります。

非同期処理の実行順序を保つ最も簡単な方法としては、コールバック関数があります。非同期処理の対象にコールバック関数を登録しておき、後から実行してもらうことで、思った通りの順序で実行することが可能になります。

addEventListenerイベントに対応するコールバック関数を登録するメソッドです。XHRのイベントに対応した関数を登録することで、イベントが発生したときに関数を実行することができます。この例ではloadイベントが発生したときに、レスポンスの内容を表示するように指定しています。

コールバック地獄

コールバック関数は優れた解決法のように思えます。ですが現実はもう少し複雑です。たとえば「foo.txt」「bar.txt」「baz.txt」を順番にひとつずつ読み込むコードを考えてみましょう。素直に書くと……

ええと、正直言ってわけがわかりません。一体何をしているのでしょうか。頑張って少し綺麗なコードにしてみましょう。

これでもまだ複雑です。コードのネストが深く、処理を頭で追いにくくなっています。また、今後処理が増えた場合には、更にネストが深くなってしまいます。こういったコールバック関数の入れ子状態のことを俗に「コールバック地獄」と言います。コールバック地獄はコードの品質を低下させ、様々な問題を発生させます。

非同期処理を記述するPromise

今までは、コールバック地獄を解決する方法はほとんどありませんでした。しかし今は違います。今では、非同期処理を簡単に扱うために導入されたPromiseという仕組みを使うことで解決できます。Promiseは非同期処理を簡潔に記述することができ、コードが単純になります。まずは例を見てみましょう。

Promiseの仕組み自体は単純です。まずPromiseをnewすることによりPromiseオブジェクトを作成します。Promiseのコンストラクタには、実行したい処理を書いた関数を渡します。処理が済んだら、resolve関数を呼び出すことで終了を明示します。

そしてPromiseオブジェクトのthenメソッドに、Promise終了後に処理したい関数を渡します。これで「Promiseの実行が済んだ後にxxxする」という処理を書くことができます。

このPromiseを利用して非同期処理を記述してみましょう。

この例は、500ミリ秒後に「hello」と表示し、その後に「world!」を表示するコードになっています。

このコードのPromise内では、500ミリ秒後に「hello」を表示し、同時にresolve関数でPromiseを終了させています。Promiseがresolveされると、thenメソッドに登録した関数が呼ばれ、「world!」が表示されます。

Promiseを利用すると、このようにして、非同期処理の実行順序を簡潔に記述することができます。

 

Promiseを終了するresolve関数

resolve関数はPromiseを終了させます。resolve関数には値を渡すこともでき、戻り値のような役割を果たします。

resolve関数に渡した値は、thenメソッドで受け取ることができます。

resolve関数を利用した例として、XHRをPromiseで書いてみましょう。

この例ではXHRのレスポンスをresolve関数に渡しています。

Promiseをエラー終了するreject関数

reject関数はresolve関数とは違い、Promiseをエラー終了させます。reject関数にはresolve関数と同様に値を渡すことができます。

reject関数が呼ばれた場合は、thenではなくcatchメソッドで受け取ります。Promiseがrejectされた場合、thenに登録した関数は呼ばれません。

thenチェーン

今までの例だけでは、Promiseの利点がほとんどわからないと思います。なので、もう少し複雑な例を考えてみましょう。500ミリ秒ごとに「hello」「world」「lorem」「ipsum」と表示するコードを書きたいとします。Promiseを使うと以下のように書くことができます。

そうです。thenの後に更にthenをつなげて書くことができるのです。thenメソッドをチェーンすることで、複数の非同期処理を直列に書くことができます。

Promiseを使ってコールバック地獄を避ける

コールバック地獄に陥っていたXHRの例をもう一度見てみましょう。

何度見ても酷いコードです。これをPromiseを使って書きなおしてみましょう。すると以下のようになります。

コードがずいぶん綺麗になりました。入れ子構造が消え去り、thenによるチェーンに置き換わることで、読みやすくなったはずです。また、ここから更に処理が増えたとしても、thenをつなげていくだけなので、これ以上複雑にはなりません。

Promiseを使うことで、コールバック地獄から逃れることができるのです。

複数のPromiseの終了を待つPromise.all

thenをチェーンして直列に書く他にも、Promise.allを利用して複数のPromiseの終了を待つこともできます。XHRの例をPromise.allを利用したものに書き換えてみます。

Promise.allにPromiseの配列を渡すことで、複数のPromiseの終了を待つPromiseを作ることができます。このPromiseの終了結果は配列になります。

async/await

Promiseを利用することで非同期処理を簡単に書けることはわかりました。しかし書き方が少し特殊です。そこで非同期処理をもっと簡潔に書けるように、async/awaitという機能が導入されました。

async関数とawait

awaitはPromiseを同期的に展開する(ように見せかける)機能です。Promiseの前にawaitを書くことで、Promiseの終了を待つことができます。

awaitキーワードはPromiseの終了を待ち、その結果を展開します。つまりこのコードは、以下のコードと同じような動きをします。

awaitがPromiseにつけられた場合、Promiseが終了するまで、コードの実行は先に進みません。awaitを利用することで、非同期処理をまるで同期処理のように書くことができます。

ただし、awaitの使用には条件があります。asyncがついた関数の中でしか利用できないのです。つまり、トップレベルでのawaitの使用は不可能です。上の例を実際に動作する形に書き直すと以下のようになります。

async/awaitを使って同期的に書く

Promiseを利用したXHRのファイル読み込みの例をもう一度見てみましょう。「foo.txt」「bar.txt」「baz.txt」を順番通りにひとつずつ読み込むコードです。

Promiseを使っているので簡単にはなっていますが、Promise特有の書き方をしているので、すこしとっつきにくさがあります。これをasync/awaitを使って書きなおしてみます。

非常に同期的なコードになりました。Promiseだけを使ったコードより、更に読みやすくなっています。Promiseとasync/awaitを組み合わせることで、非同期処理を非常に単純に書くことができるようになるのです。