Skip to content

Latest commit

 

History

History
287 lines (217 loc) · 11.9 KB

race-delay-timeout.adoc

File metadata and controls

287 lines (217 loc) · 11.9 KB

Promise.raceとdelayによるXHRのキャンセル

このセクションでは2章で紹介したPromise.raceのユースケースとして、 Promise.raceを使ったタイムアウトの実装を学んでいきます。

もちろんXHRは timeout プロパティを持っているので、 これを利用すると簡単にできますが、複数のXHRを束ねたタイムアウトや他の機能でも応用が効くため、 分かりやすい非同期処理であるXHRにおけるタイムアウトによるキャンセルを例にしています。

Promiseで一定時間待つ

まずはタイムアウトをPromiseでどう実現するかを見ていきたいと思います。

タイムアウトというのは一定時間経ったら何かするという処理なので、setTimeout を使えばいいことが分かりますね。

まずは単純に setTimeout をPromiseでラップした関数を作ってみましょう。

delayPromise.js
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.race について簡単に振り返ると、 以下のようにどれか一つでもpromiseオブジェクトが解決状態になったら次の処理を実行する静的メソッドでした。

link:../Ch2_HowToWrite/embed/embed-promise-race-other.js[role=include]

先ほどのdelayPromiseと別のpromiseオブジェクトを、 Promise.race によって競争させることで簡単にタイムアウトが実装できます。

simple-timeout-promise.js
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オブジェクト

Error オブジェクトはECMAScriptのビルトインオブジェクトです。

ECMAScript5では完璧に Error を継承したものを作ることは不可能ですが(スタックトレース周り等)、 今回は通常のErrorとは区別を付けたいという目的なので、それを満たせる TimeoutError オブジェクトを作成します。

Note

ECMAScript 2015から class 構文を使うことで内部的にも正確に継承を行うことができます。

class MyError extends Error {
    // Errorを継承したオブジェクト
}

error instanceof TimeoutError というように利用できる TimeoutError を定義すると 以下のようになります。

TimeoutError.js
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オブジェクトについて書かれています。

タイムアウトによるXHRのキャンセル

ここまでくれば、どのようにPromiseを使ったXHRのキャンセルを実装するか見えてくるかもしれません。

XHRのキャンセル自体は XMLHttpRequest オブジェクトの abort() メソッドを呼ぶだけなので難しくないですね。

abort() メソッドを外から呼べるようにするために、今までのセクションにもでてきたfetchURLを少し拡張して、 XHRを包んだpromiseオブジェクトと共にそのXHRを中止するメソッドをもつオブジェクトを返すようにしています。

delay-race-cancel.js
link:embed/embed-delay-race-cancel.js[role=include]

これで必要な要素は揃ったので後は、Promiseを使った処理のフローに並べていくだけです。 大まかな流れとしては以下のようになります。

  1. cancelableXHR を使いXHRのpromiseオブジェクトと中止を呼び出すメソッドを取得する

  2. timeoutPromise を使いXHRのpromiseとタイムアウト用のpromiseを Promise.race で競争させる

    • XHRが時間内に取得できた場合

      1. 通常のpromiseと同様に then で中身を取得する

    • タイムアウトとなった場合は

      1. throw new TimeoutError されるので catch する

      2. catchしたエラーオブジェクトが TimeoutError のものだったら abort を呼び出してXHRをキャンセルする

これらの要素を全てまとめると次のように書けます。

delay-race-cancel-play.js
link:embed/embed-delay-race-cancel-play.js[role=include]

これで、一定時間後に解決されるpromiseオブジェクトを使ったタイムアウト処理が実現できました。

Note
通常の開発の場合は繰り返し使えるように、それぞれファイルに分割して定義しておくといいですね。

promiseと操作メソッド

先ほどのcancelableXHRはpromiseオブジェクトと操作のメソッドが 一緒になったオブジェクトを返すようにしていたため少し分かりにくかったかもしれません。

一つの関数は一つの値(promiseオブジェクト)を返すほうが見通しがいいと思いますが、 cancelableXHR の中で生成した req は外から参照できないので、特定のメソッド(先ほどのケースは abort)からは触れるようにする必要があります。

返すpromiseオブジェクト自体を拡張して abort できるようにするという手段もあると思いますが、 promiseオブジェクトは値を抽象化したオブジェクトであるため、何でも操作用のメソッドをつけていくと複雑になってしまうかもしれません。

一つの関数で全てやろうとしてるのがそもそも良くないので、 以下のように関数に分離していくというのが妥当な気がします。

  • XHRを行うpromiseオブジェクトを返す

  • promiseオブジェクトを渡したら該当するXHRを止める

これらの処理をまとめたモジュールを作れば今後の拡張がしやすいですし、 一つの関数がやることも小さくて済むので見通しも良くなると思います。

モジュールの作り方は色々作法(AMD,CommonJS,ES module etc..)があるので ここでは、先ほどの cancelableXHR をNode.jsのモジュールとして作りなおしてみます。

cancelableXHR.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)
  1. XHRをラップしたpromiseオブジェクトを作成

  2. 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();

AbortControllerという今回実装したものと似たような操作メソッドをもつオブジェクトを利用することがわかります。