DOMによるWeb Application Clientの可能性

非同期は、三種の神器(Promise・async・await)で成敗!

JavaScriptが、だれでも簡単に動的なHTMLコンテンツを作成できる「ちょっとした道具」であった時代は遠い過去となり、今では本格的なアプリケーションを実現できるレベルの「強力な道具」にまで発展してきました。

ネットワークアクセスはもとより、ローカルファイルへのアクセスやあまつさえデータベースまで!

そして、それらは全て、非同期非同期非同期非同期非同期!!!!!!!

「いいかげんにしてくれ〜〜〜〜!!」

幸いというか、世界中に同じ様に感じた人たちが沢山いたと見えて、解決策が提示されています。

そのあたりをちょっと紹介します。

少々意地悪なサンプル

やりたいことはこうです。

XMLHttpRequestでリクエストし、その結果をJSONとして評価してfileNameプロパティよりファイル名を取得します。

そのファイル名に再度リクエストを行い、その結果をイメージのデータURLとして画面に表示します。

非同期呼び出しの嵐です。

背景色が変わっている箇所がイベントリスナの登録箇所ですが、あくまでもこれは正常ケースのみです。

本来であれば、ここにまだ"error"のイベントリスナ登録(場合によっては、"abort"のイベントリスナ登録も)と、HTTPステータスが200番代以外のエラーケースを追加する必要があります。

ちょっとうんざりですね。

まだ、処理が分割できればよいのですが、普通にコーディングするとネストしてしまうので、解りやすく分割するのは結構厄介です。

Promise参上

一つ前のFetchの章で登場したPromiseですが、そもそもこの様な非同期ネスト地獄(callback hell:コールバック地獄というらしい)をなんとかしようと考えられたものです。

なんとかしてもらいましょう。

Promiseチェーンに関しては前の章でも簡単に照会しましたが、コードが解りやすくも見やすくもならないので(個人の感想です)、ここでは非同期関数とawait演算子により実装します。

Promiseによる、レガシーのラッピング

XMLHttpRequestは、Promiseによる実装としてFetchが利用できますが、FileReaderは代替手段が存在しないので作成します。共通関数として汎用的に利用できるように実装します。

上記の関数は、FileReaderの処理をPromiseでラップした関数で、引数で渡されたblobDataURLに変換して返します(実際の返却値は、DataURLをラップしたPromiseになります)。

処理が成功した時にresolve(解決)、エラーが発生した時にreject(拒否)をそれぞれ呼び出しています。

resolveが呼び出されると、resolveに渡した引数の内容Promiseでラップされて返却されます。

rejectが呼び出されると、rejectに渡した引数の値がスローされます。

非同期関数本体の実装

下記が、非同期関数の本体です。

関数宣言の前にasyncを付けることで、非同期関数となります。

非同期関数(async付きの関数)の中でawait演算子を付けてPromiseを返す関数を実行すると、resolverejectのいずれかが呼び出されるまで、処理を停止します。

3行目fetchを使ってサーバにリクエストしています。awaitを付けているので、レスポンスが返るまで処理が停止します。

ネットワークエラーなどが発生した場合は、この呼び出しでエラーがスローされます。

レスポンスが返ると、4行目で結果を確認します。HTTPステータスが200番代で返るとresponse.oktrueとなるので、200番代以外のステータスが返った場合にエラーをスローします。

7行目でレスポンスからjsonオブジェクトを取得しています。

response.json()jsonオブジェクトをラップしたPromiseを返すので、awaitを付けて呼び出すことで、jsonオブジェクトそのものを取得します。

それ以降は、ほぼ同じ処理の繰り返しですが、14行目で先に定義した共通関数(readAsDataURL)を呼び出しています。

全体をtry〜catchで囲っていますが、このcatchで非同期処理の中で発生したエラーを含め、全てのエラーがキャッチできます。

先のXMLHttpRequestを使ったサンプルと同じことを実装している上に、エラー処理も取り込んでいる割には簡潔に記述できていると思うのですが、いかがでしょう。

追いかけっこ

Promiseasyncawaitのおかげで楽になった処理に、スリープがあります。特にループ内で一定時間スリープさせる処理を、簡単に記述できるようになりました。

setTimeoutをラッピング

先ずは、setTimeoutPromiseでラップした関数を用意します。

スリープを目的としているので、第2引数はオプションです。指定されている場合にも呼び出します。

ループ内でsleepするサンプル

これを使って、簡単な追いかけっこプログラムを作ってみました。

殆ど説明する箇所はありませんね。ループの中で、プレイヤー要素の左側のマージンを大きくしているだけです。

大きくするマージンの量は、疑似乱数を発生させて決定しています。

15行目17行目で先に作ったsleep関数を呼び出しています。今回は一定時間処理をスリープさせることが目的なので、引数は1つです。

chase関数は非同期に実行されるので、引数を変えて順に呼び出すだけで、平行(並列ではありません)に動作します。

ご意見・ご質問