Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

JSDeferredの動きを追ってみた

JSDeferredを使ってみたメモです。動きがわかれば、使うときに気をつけなきゃならないツボがわかるようになります。

なお、詳しい解説は本家をどうぞ。

JSDeferredはどんなもの?

チュートリアル的ではなく、このフレームワークの基本中の基本を最初に見ておきます。

JSDeferredでは、処理を数珠つなぎにして実行できます。以下、実用的にはまったく役に立ちませんが、このフレームワークの基本中の基本となります。

// Deferredを作る
var d1 = Deferred();
// 処理をつないでいく
var d2 = d1.next(function () { alert("d2"); });
var d3 = d2.next(function () { alert("d3"); });

// 発火すると全部走る
d1.call();

ただ、これではまったく非同期性がありません。ここに非同期性を持ち込まないと、このフレームワークはちっとも楽しくありません。

クラスメソッドとインスタンスメソッドのnextは違う

非同期性を持ち込むものの1つが、 Deferred.next() です。Deferred.define() している場合は、単なる next() のことです。

var d1 = Deferred.next();
var d2 = d1.next(function () { alert("d2"); });
var d3 = d2.next(function () { alert("d3"); });

実はこのnextは、最初の例に出て来たインスタンスメソッドの next() とは働きが違います。クラスメソッドとしての next() は、最初の例の「Deferred()」と「d1.call()」の処理に相当します。

しかし、d1を作った時点でいきなり call() を呼ぶと、インスタンスメソッドの next() で処理を数珠つなぎにする前に発火してしまうことになりますので、何も処理が走りません。そこで、 Deferred.next() では、 d1.call() の呼び出しをsetTimeoutを使って遅延させています。これが Deferred たる所以です。

setTimeoutを使うことで、処理の順番は、

  1. d1の作成
  2. d2、d3のnextの処理(数珠つなぎする)
  3. ブロックを抜けたら、setTimeoutでキューイングされたd1.callが走る

と、一番最初の例の呼び出し順と同じになります。

Deferred同士をつなぐ

var d1 = Deferred.next();
var d2 = Deferred.next();

このようにクラスメソッドのnext()で作った2つのDeferredをつなぐことはできません。Deferredのチェーンは、インスタンスメソッドのnextによって作ります*1

var d1   = Deferred.next();
var d1_2 = d1.next(function () { /* NOP */ });
var d1_3 = d1_2.next(function () { alert("d1_3"); });
var d2   = Deferred.next();
var d2_2 = d2.next(function () { alert("d2_2"); });

/*
d1のチェーンは、 d1 -> d1_2 -> d1_3 。
d2のチェーンは、 d2 -> d2_2 。
*/

さて、これでチェーンを2本作ることはできましたが、やはりd1のチェーンとd2のチェーンはこのままではつながりません。

そこで、d2の作成をd1のチェーンの中で行うことで、擬似的に*2d1のチェーンにd2のチェーンをつなげることができます。

var d1   = Deferred.next();
var d1_2 = d1.next(function () {
    var d2   = Deferred.next();
    var d2_2 = d2.next(function () { alert("d2_2"); });
});
var d1_3 = d1_2.next(function () { alert("d1_3"); });

/*
d1のチェーンは、 d1 -> d1_2 --> d1_3 。
                     | ←ここは非同期
d2のチェーンは、            +-> d2 -> d2_2
*/

ところが、これですとまだチェーンがd1とd2 の2つに分かれているため、 d1_3 と d2_2 の実行順が定まりません。

そこで、 next() に渡すコールバックの戻り値として、 Deferred である d2_2 を指定します。こうすると、JSDeferredは、これ以上チェーンを辿るのをやめ、新しいチェーンに残りの処理 (d1_3) をつなぎ直します。すると、以下のようになります。

var d1   = Deferred.next();
var d1_2 = d1.next(function () {
    var d2   = Deferred.next();
    var d2_2 = d2.next(function () { alert("d2_2"); });
    return d2_2;
});
var d1_3 = d1_2.next(function () { alert("d1_3"); });

/*
d1のチェーンは、 d1 -> d1_2 + (※ ここにあったd1_3は、実行せずに移動)
                     | ←ここは非同期
d2のチェーンは、            +-> d2 -> d2_2 -> d1_3 。
*/

実際は2本のチェーンなのですが、非同期呼び出しを経由して1本のチェーンっぽくなりました*3。これで意図した順番に動きます。このように、Deferredのチェーン中に別のDeferredのチェーンを埋め込むには、Deferredをreturnしてチェーンを組み替えさせます。

まとめると、チェーンをつなげるポイントは以下です。

  • チェーン中で新しいチェーンを生成する
  • 作ったチェーンをreturnし、元のチェーンと合わせて組み替えてもらう

JSDeferred.next() に代わるもの

JSDeferred.next() からチェーンを作り始めることで、非同期性を持ち込むことができました。ですが、これと同じものを自分で作ってこそ、JSDeferred の真価を発揮できます。

チェーンの先端となれるような JSDeferred を作るときの最大のポイントは「Deferredオブジェクト d を作って返すが、そのdに対して後から(チェーンをつなぎ終わってから) d.call() が呼ばれること」です。 JSDeferred.next() では、setTimeout() を使ってこの要求を実現していました。

JavaScriptでは「遅れて呼ばれる」仕組みの代表としてもう1つ、XMLHttpRequest が挙げられます*4。これを使って、チェーンの先端になれる JSDeferred を返す関数を作ってみると、こうなります。

function httpget(url){
    var d = Deferred();

    var xhr = new XMLHttpRequest();
    xhr.open("GET", "test.txt", true);
    xhr.onreadystatechange = function () {
        if(xhr.readyState == 4) d.call(xhr.responseText);
    };
    xhr.send(null);

    return d;
}

実際にcall()が呼び出されるのは、Ajaxリクエストが終了してからです。よってcallが呼び出される(=発火する)前に、以下のように .next() によって返却したDeferredオブジェクトにチェーンをつなぐ処理を完了することができます。

httpget().next(function (text) {
    alert(text);
});

このように、非同期処理を「JSDeferredを返し、後からcall()でそれに発火してくれる処理」としておくと、うまくJSDeferredフレームワークに適合させることができます。

まとめ

以下の2つの基本を理解して、楽しいJSDeferredライフを!!

  • JSDeferred.next() でチェーンを始められる
    • JSDeferred.wait() とかも、同類
    • 同様のメソッドも自分で作れる(チェーンの起点とできる)
  • チェーン同士は、片方のチェーンからもう片方のチェーンをreturnすればつながる

最後に一言。JSDeferred.next()ライクな関数を作るときも、チェーンを連結するときもDeferredオブジェクトをreturnしますが、この二つの意味は大きく違う*5ので惑わされないようにしましょう。

*1:errorでもチェーンは伸びます。

*2:ほんとはチェーンはつながってないのがポイント。チェーンがつながってないので、実行順の制御が必要になる。

*3:この単純な仕組みが非常にうまくいくのが、Deferredのすごいとこだと思います。

*4:他、onClick等のUIイベント系もありますが、JSDeferred化して幸せかはどうなんでしょう? ボタンを決まった順番に押す場合とか?

*5:ユーザに返すかフレームワークに返すかの違い。