このセクションでは2章で紹介したPromise.race
のユースケースとして、
Promise.raceを使ったタイムアウトの実装を学んでいきます。
もちろんXHRは timeout プロパティを持っているので、 これを利用すると簡単にできますが、複数のXHRを束ねたタイムアウトや他の機能でも応用が効くため、 分かりやすい非同期処理であるXHRにおけるタイムアウトによるキャンセルを例にしています。
まずはタイムアウトをPromiseでどう実現するかを見ていきたいと思います。
タイムアウトというのは一定時間経ったら何かするという処理なので、setTimeout
を使えばいいことが分かりますね。
まずは単純に setTimeout
をPromiseでラップした関数を作ってみましょう。
link:embed/embed-delayPromise.js[role=include]
delayPromise(ms)
は引数で指定したミリ秒後にonFulfilledを呼ぶpromiseオブジェクトを返すので、
通常の setTimeout
を直接使ったものと比較すると以下のように書けるだけの違いです。
setTimeout(() => {
alert("100ms 経ったよ!");
}, 100);
// == ほぼ同様の動作
delayPromise(100).then(() => {
alert("100ms 経ったよ!");
});
ここではpromiseオブジェクトであるということが重要になってくるので覚えておいて下さい。
Promise.race
について簡単に振り返ると、
以下のようにどれか一つでもpromiseオブジェクトが解決状態になったら次の処理を実行する静的メソッドでした。
link:../Ch2_HowToWrite/embed/embed-promise-race-other.js[role=include]
先ほどのdelayPromiseと別のpromiseオブジェクトを、
Promise.race
によって競争させることで簡単にタイムアウトが実装できます。
link:embed/embed-simple-timeout-promise.js[role=include]
timeoutPromise(比較対象のpromise, ms)
はタイムアウト処理を入れたい
promiseオブジェクトとタイムアウトの時間を受け取り、Promise.race
により競争させたpromiseオブジェクトを返します。
timeoutPromise
を使うことで以下のようにタイムアウト処理を書くことができるようになります。
link:embed/embed-simple-timeout-promise.js[role=include]
// 実行例
var taskPromise = new Promise(function(resolve){
// 何らかの処理
var delay = Math.random() * 2000;
setTimeout(function(){
resolve(delay + "ms");
}, delay);
});
timeoutPromise(taskPromise, 1000).then(function(value){
console.log("taskPromiseが時間内に終わった : " + value);
}).catch((error) => {
console.log("タイムアウトになってしまった", error);
});
タイムアウトになった場合はエラーが呼ばれるようにできましたが、 このままでは通常のエラーとタイムアウトのエラーの区別がつかなくなってしまいます。
この Error
オブジェクトの区別をしやすくするため、
Error
オブジェクトのサブクラスとして TimeoutError
を定義したいと思います。
Error
オブジェクトはECMAScriptのビルトインオブジェクトです。
ECMAScript5では完璧に Error
を継承したものを作ることは不可能ですが(スタックトレース周り等)、
今回は通常のErrorとは区別を付けたいという目的なので、それを満たせる TimeoutError
オブジェクトを作成します。
Note
|
ECMAScript 2015から class MyError extends Error {
// Errorを継承したオブジェクト
} |
error instanceof TimeoutError
というように利用できる TimeoutError
を定義すると
以下のようになります。
link:embed/embed-TimeoutError.js[role=include]
TimeoutError
というコンストラクタ関数を定義して、このコンストラクタにErrorをprototype継承させています。
使い方は通常の Error
オブジェクトと同じで以下のように throw
するなどして利用できます。
const promise = new Promise(() => {
throw new TimeoutError("timeout");
});
promise.catch((error) => {
console.log(error instanceof TimeoutError);// true
});
この TimeoutError
を使えば、タイムアウトによるErrorオブジェクトなのか、他の原因のErrorオブジェクトなのかが容易に判定できるようになります。
Note
|
今回紹介したビルトインオブジェクトを継承したオブジェクトの作成方法については Chapter 28. Subclassing Built-ins で詳しく紹介されています。 また、 Error - JavaScript | MDN にもErrorオブジェクトについて書かれています。 |
ここまでくれば、どのようにPromiseを使ったXHRのキャンセルを実装するか見えてくるかもしれません。
XHRのキャンセル自体は XMLHttpRequest
オブジェクトの abort()
メソッドを呼ぶだけなので難しくないですね。
abort()
メソッドを外から呼べるようにするために、今までのセクションにもでてきたfetchURL
を少し拡張して、
XHRを包んだpromiseオブジェクトと共にそのXHRを中止するメソッドをもつオブジェクトを返すようにしています。
link:embed/embed-delay-race-cancel.js[role=include]
これで必要な要素は揃ったので後は、Promiseを使った処理のフローに並べていくだけです。 大まかな流れとしては以下のようになります。
-
cancelableXHR
を使いXHRのpromiseオブジェクトと中止を呼び出すメソッドを取得する -
timeoutPromise
を使いXHRのpromiseとタイムアウト用のpromiseをPromise.race
で競争させる-
XHRが時間内に取得できた場合
-
通常のpromiseと同様に
then
で中身を取得する
-
-
タイムアウトとなった場合は
-
throw new TimeoutError
されるのでcatch
する -
catchしたエラーオブジェクトが
TimeoutError
のものだったらabort
を呼び出してXHRをキャンセルする
-
-
これらの要素を全てまとめると次のように書けます。
link:embed/embed-delay-race-cancel-play.js[role=include]
これで、一定時間後に解決されるpromiseオブジェクトを使ったタイムアウト処理が実現できました。
Note
|
通常の開発の場合は繰り返し使えるように、それぞれファイルに分割して定義しておくといいですね。 |
先ほどのcancelableXHR
はpromiseオブジェクトと操作のメソッドが
一緒になったオブジェクトを返すようにしていたため少し分かりにくかったかもしれません。
一つの関数は一つの値(promiseオブジェクト)を返すほうが見通しがいいと思いますが、
cancelableXHR
の中で生成した req
は外から参照できないので、特定のメソッド(先ほどのケースは abort
)からは触れるようにする必要があります。
返すpromiseオブジェクト自体を拡張して abort
できるようにするという手段もあると思いますが、
promiseオブジェクトは値を抽象化したオブジェクトであるため、何でも操作用のメソッドをつけていくと複雑になってしまうかもしれません。
一つの関数で全てやろうとしてるのがそもそも良くないので、 以下のように関数に分離していくというのが妥当な気がします。
-
XHRを行うpromiseオブジェクトを返す
-
promiseオブジェクトを渡したら該当するXHRを止める
これらの処理をまとめたモジュールを作れば今後の拡張がしやすいですし、 一つの関数がやることも小さくて済むので見通しも良くなると思います。
モジュールの作り方は色々作法(AMD,CommonJS,ES module etc..)があるので
ここでは、先ほどの cancelableXHR
をNode.jsのモジュールとして作りなおしてみます。
link:lib/cancelableXHR.js[role=include]
使い方もシンプルに createXHRPromise
でXHRのpromiseオブジェクトを作成して、
そのXHRを abort
したい場合は abortPromise(promise)
にpromiseオブジェクトを渡すという感じで利用できるようになります。
const cancelableXHR = require("./cancelableXHR");
const xhrPromise = cancelableXHR.createXHRPromise("https://httpbin.org/get");// (1)
xhrPromise.catch((error) => {
// abort されたエラーが呼ばれる
});
cancelableXHR.abortPromise(xhrPromise);// (2)
-
XHRをラップしたpromiseオブジェクトを作成
-
1で作成したpromiseオブジェクトのリクエストをキャンセル
ここでは以下のことについて学びました。
-
一定時間後に解決されるdelayPromise
-
delayPromiseとPromise.raceを使ったタイムアウトの実装
-
XHRのpromiseのリクエストのキャンセル
-
モジュール化によるpromiseオブジェクトと操作の分離
Promiseは処理のフローを制御する力に優れているため、 それを最大限活かすためには一つの関数でやり過ぎないで処理を小さく分けること等、 今までのJavaScriptで言われているようなことをより意識していいのかもしれません。
Note
|
Fetch APIでのキャンセル
XHRの現代的なバージョンである Fetch API では、 AbortController というAPIによってリクエストをキャンセルを実現できます。 Fetch APIでは、次のようにリクエストをキャンセルできます。 AbortControllerでのFetchのキャンセル
// AbortControllerのインスタンスの作成
const controller = new AbortController();
// キャンセルを通知するための siganl を取得する
const signal = controller.signal;
// signal をfetchメソッドの第二引数に渡す
fetch("https://httpbin.org/get", { signal })
.then((result) => {
// 結果の正常処理
console.log(result);
})
.catch((error) => {
if (error.name == "AbortError") {
// 中断の場合の処理
console.error("Fetchが中断されました", error);
return;
}
// 中断以外のエラー
console.error(err);
});
// Fetchをキャンセルする
controller.abort();
|