##とりあえず打ち込みましょう
F12キーを叩いてブラウザの開発者環境を開いて、コンソールにvar p = fetch('https://test-endpoint.azurewebsites.net/api/company?delay=10')
と打ち込みます。
ここでは、 fetch
関数を使って指定したサーバーのエンドポイント(Azure Functionでテスト用として立てました)にリクエストを送信して、サーバーからのレスポンスを取ってくるようにしています。
実はこのエンドポイント、delay
というパラメータを渡すことでサーバー側でわざと時間がかかるように細工をしています。delay=10
の場合10秒間スリープ状態にさせて、それからJSONのデータを含むレスポンスを返すようにしています。反応が悪いAPIをシミュレートしているわけです。
レスポンスが返ってくるまで最低でも10秒かかるはずのコードなのに「一瞬」で変数p
に戻り値が格納されました。開発環境コンソールが10数秒固まってしまう、というようなことにもなりませんでした。なぜなら、変数p
にはレスポンスではなく、「レスポンスの結果にいつでもアクセスできますよ」というPromise(約束)が代わりに格納されたからです。
##「10秒間経ってしまう前」と「10秒間経ってしまった後」のPromise
fetch()
を呼び出してすぐ(10秒以内)、そして10秒以上待ったあとにもう一度、p
の値をコンソールに表示してみましょう。
1回目と2回目の違いは・・・
- 「10秒間経ってしまう前」pendingというステータスで、responseというプロパティはない
- 「10秒間経ってしまった後」resolvedというステータスで、responseというプロパティがある
このようにPromiseは「ある仕事を任された請負人との約束」のように機能し、任された仕事の進捗状況と、さらに仕事が終わったならその結果へのアクセスを可能にする、という特徴があります。
##ここまでのおさらい
-
fetch関数
のように外部(サーバーなど)の応答を待つ関数はPromise
を返すことが多い -
呼び出し元は、そのような関数からPromise
をすぐに受け取り、ブロックされることはない -
Promise
には状態があって、時間の経過とともに変化する -
Promise
のスタータスがpending
なら、仕事はまだ終わっていない -
Promise
のステータスがresolved
なら、仕事は終わっていて、結果へのアクセスが出来る
Promise の結果を受け取る
Promiseから仕事の結果を受け取るコードを書いてみましょう。
Promiseのthen()
という関数を呼び出しています。then()
には、Promiseの結果(この場合はresponse)を受け取って処理するための関数を渡しています。
処理はresponseクラスのjson()
というメソッドを呼び出すことで、レスポンスからJSONペイロードを抽出するようにしています。その結果をさらに返して、p2
という別の変数に格納しました。
ここでp2
もPromiseであることに注意してください。これはresponseクラスの仕様によると、json()
メソッドもPromiseを返すとあります。
Responseクラスのメソッドのうち、ストリームを読むメソッドは全部Promiseを返します。ストリームを読むという「仕事」も時間がかかる場合があるのでPromiseの仕組みを利用しているというわけです。この種の関数やメソッドはPromiseを多用していてよく見かけます。
では最後にp2
に格納されているPromiseの結果を受け取りに行きましょう。
ようやくAPIからのレスポンスに含まれていたJSONデータを取得できました。
##ここまでのコードのまとめ
下の3行のコードをそのままコンソールにコピペしてみましょう。
var p = fetch('https://test-endpoint.azurewebsites.net/api/company?delay=10')
var p2 = p.then(function(response) { return response.json(); })
p2.then(function(json) { console.log(json); })
結果とどうなりましたか?10秒ほどたってから突然、JSONのデータがコンソールに表示されたはずです。
ここで重要なのは、このコードは2つのPromise(この場合p
とp2
)の状態を気にせずに書くことが出来ることです。それぞれのPromiseが一瞬で終わろうとも、数10秒かかろうとも(エラー発生の場合は除く。エラー対策に関しては後述)同じコードです。
##コードをさらに整理する
then()
関数はPromiseを返すので、変数p
やp2
を省略してひとつの制御文に整理することができます。
fetch('https://test-endpoint.azurewebsites.net/api/company?delay=10')
.then(function(response) { return response.json(); })
.then(function(json) { console.log(json); })
結果は全く同じで、10秒後にJSONデータが表示されました。ここでは、fetch()
関数が返すPromiseのthen()
関数を呼び出し、その戻り値であるPromiseに対して、さらにthen()
関数を呼び出す、という風にチェーンにしています。
チェーンの順番が、実行の順番と同じなのでより自然に感じられます。
##エラーへの対応
ところで、例えばサーバーがデータを返さなかったり、JSONのデータが壊れていてパース出来ない、といった状況の場合はいったいどうしたらいいのでしょう?特に複数のPromiseをチェーンにしている時は、どこでエラーが起こるやもしれません。
ということで、エラーハンドラーを仕掛けましょう。catch()
関数を最後に呼び出すことで、一種の例外処理を行います。
fetch('https://test-endpoint.azurewebsites.net/api/company?delay=10')
.then(function(response) { return response.json(); })
.then(function(json) { console.log(json); })
.catch(function(err) { console.log("something went wrong: " + err.message); })
サーバーをいったんオフラインにして、実行してみるとこのような結果になりました。
これはjson()
メソッドが「パースするJSONが無いんですけど」と文句を言っている状況です。
ちなみに、サーバーをOFFにしたので404が返ってきましたが、これは1つめのPromiseのステータスが
rejected
になったわけではありません。ちゃんと404のレスポンスという「仕事の結果」が得られたのでPromiseのステータスもresolved
になっています。この例ではこの1つ目のPromiseの結果の処理において、エラーが発生した、ということです。
##おさらい、その2
- Promiseから仕事の結果を得るには
then()
関数を呼び出す -
then()
関数には、結果を受け取る関数を定義して渡す - Promiseの仕組みはネットワークへのアクセスやI/O関連のAPIで多用されている
- Promiseの仕組みを使うと「いつ仕事の結果が得られる状態になるか」を気にせずにコードを書ける。その時が来たら
then()
に渡した関数が実行を始める。それは0.1ミリ秒後かもしれないし、10秒後かもしれない。 -
then()
はPromiseを返すので、一連の処理をチェーンにして正しい順序を指定することが出来る - チェーンにした一連の処理は書く順序と実行する順序が同じなので自然で読みやすい
- エラー対策として
catch()
関数を利用できる
##発展
この記事ではPromiseの利用に関して書きました。Promiseの作り方についてはまた別の機会で(もしくはここを拡張するかもしれません)。
ここまでの内容がわかったらPromise.all
の関数を見ると楽しいでしょう。
##おまけ
アロー関数式を使ってさらに整理するともっとスッキリしますね。Promiseとは関係ないですが。
fetch('https://test-endpoint.azurewebsites.net/api/company?delay=10')
.then(response => { return response.json(); })
.then(json => { console.log(json); })
.catch(err => { console.log("something went wrong: " + err.message); })