Pot.js Deferred リファレンス

Deferred リファレンス

このセクションでは、Pot.Deferred について解説します。

チェイン

Pot.Deferred は JavaScript の非同期処理を扱い、 また直列的に書けるよう作られています。

setTimeout(function() {
    something1();
    setTimeout(function() {
        something2();
        setTimeout(function() {
            something3();
        }, 0);
    }, 0);
}, 0);

このように深くなってしまうネストを、

begin(something1).then(something2).then(something3);

というふうに書けるようになります。

シンプルな非同期処理を Pot.Deferred を使い書いてみます。

begin(function() {
    console.log('begin');
    return wait(2);
}).then(function() {
    console.log('end');
});

これは、begin によって非同期で処理が始まり コンソールに 'begin' と出力した後、2 秒待ってから 'end' と出力されます。

Pot.globalize() の適応がされてない場合は、

Pot.Deferred.begin(function() {
    console.log('begin');
    return Pot.Deferred.wait(2);
}).then(function() {
    console.log('end');
});

という記述になります。

(以降は便宜上 Pot.globalize() がなされた記述で例を示します。)

他の Deferred ライブラリの経験がある場合、 関数名の違いに違和感を感じるかもしれないので以下に示します。 (いくつか解釈が違うものもあります。)

Pot.Deferred MochiKit.Async.Deferred JSDeferred jQuery.Deferred dojo.Deferred
then addCallback(s) next done / then then / addCallback(s)
rescue addErrback error fail addErrback
ensure addBoth - always addBoth
begin callback call resolve resolve / callback
raise errback fail reject reject / errback
cancel cancel cancel - cancel

Pot.Deferred は MochiKit.Async.Deferred および JSDeferred とほぼ同じ動作をします。


以降の解説に関して、Deferred の基本的なものは JSDeferred のドキュメント を参考にしています。 ゆえに、内容がそっくりになってしまっていますのでご了承ください。 なお、問題等ありましたら 連絡先 までお願いします。 JSDeferred の解説はとてもわかりやすいので、Deferred に関して情報を得たい場合、 一度は閲覧することをお勧めします。

非同期処理は、setTimeout だけでなく Ajax や onclick などのイベント時にも発生します。
例えば request という関数が、第一引数に URL を、 第二引数にコールバック関数をとるものとします。
これを Deferred なしで書くと、

request('/foo.json', function(fooData) {
    request('/bar.json', function(barData) {
        request('/baz.json', function(bazData) {
            alert([fooData.result, barData.result, bazData.result]);
        });
    });
});

リクエストが増えれば増えるほどネストが深くなってしまいます。

これを Pot.Deferred を用いて書くと、

// request は第一引数に URL をとり Deferred が返る関数として
var result = [];
begin(function() {
    return request('/foo.json').then(function(fooData) {
        result.push(fooData.result);
    });
}).then(function() {
    return request('/bar.json').then(function(barData) {
        result.push(barData.result);
    });
}).then(function() {
    return request('/baz.json').then(function(bazData) {
        result.push(bazData.result);
    });
}).then(function() {
    alert(result);
});

他にも多様な書き方がありますが、処理が直列的になりました。

しかし、この例の場合 同じような処理を繰り返してしまっています。
未知数のリクエストに対応させると、

var urls = ['/foo.json', '/bar.json', '/baz.json'];
var result = [];
Deferred.forEach(urls, function(url) {
    return request(url).then(function(data) {
        result.push(data);
    });
}).then(function() {
    alert(result);
});

こんな感じに書くことができます。 Deferred.repeat を使ってもいいと思います。

さらにコンパクトかつ、次の処理待ちという縛りをなくし 同時実行 (並列処理) として書くなら、

parallel([
    request('/foo.json'),
    request('/bar.json'),
    request('/baz.json')
]).then(function(result) {
    alert(result);
});

このように、parallel (Pot.Deferred.parallel) を使うこともできます。 parallel は、MochiKit でいう DeferredList と同等の処理を可能とします。 すべての処理が終わった後、次のコールバックが呼ばれます。 parallel は、オリジナルの JSDeferred.parallel をベースに実装されています。


各チェインのコールバック関数の結果は、 return した値が次のチェインの引数として渡されます。
コールバック関数から return されなかった場合、 結果値として無視されます。
その場合、前の結果値を引継ぎ次のコールバックに再び渡されます。
コールバック関数スコープ直下に return があるかないかで判断されます。

begin(function() {
    return 'foo';
}).then(function(res) {
    alert(res); // 'foo'

    // 値を返さずこのコールバックを抜ける

}).then(function(res) {
    // 前のチェインの結果が引き継がれる
    alert(res); // 'foo'

    // 値を返す
    return 'bar';
}).then(function(res) {
    alert(res); // 'bar'
});

例外処理

非同期処理の中で発生した例外は、 何事もなかったかのように振舞われてしまいます。

Pot.Deferred では、これを rescue というメソッドにより捕捉が可能となっています。

begin(function() {
    alert('begin');
}).then(function() {
    // 定義されていない関数を呼び例外が発生
    undefinedFunc.call();
}).rescue(function(err) {
    // 例外をキャッチ
    alert(err);
}).then(function() {
    // その後も処理を続けられる
    alert('end');
});

大まかに、このような流れで処理ができます。

rescue によるキャッチは、なにも必ず次のチェインでなくても捕捉できます。

begin(function() {
    alert(1);
    return 1 + 1;
}).then(function(res) {
    alert(res); // 2
    return res + 1;
}).then(function(res) {
    alert(res); // 3

    // エラーを発生させる
    throw new Error('error');
}).then(function(res) {
    // エラーが発生したためこのコールバックは実行されない
    alert(res);
    return res + 1;
}).rescue(function(err) {
    // エラーをキャッチ
    alert(err);
    return 'end';
}).then(function(res) {
    alert(res); // 'end'
});

この例では、 1, 2, 3, 'error', 'end' とアラートされます。

then の第一引数のコールバック関数は、成功時のみ実行され エラーが発生した場合は無視されます。


実際のところ、チェインに必ず rescue を入れるような丁寧な実装は そう多くはないと思います。
そのため、エラーが発生していても気づかず実装完了してしまうことがあるかもしれません。
Pot.Deferred は そのような事例を防ぐため、 rescue などによりキャッチされずに終了してしまったチェインは Pot.Deferred 側から再び throw するようになっています。
この再スロー機能により、例外を逃さず発見することが可能になっています。


例外時、成功時の両方 つまり何が起きても次のチェインを実行したい場合は、 ensure メソッドを使います。

begin(function() {
    return 1;
}).then(function(res) {
    alert(res);
    // ランダムに例外を発生させる
    if (Math.random() * 10 < 5) {
        throw new Error('error');
    } else {
        return res + 1;
    }
}).ensure(function(res) {
    if (isError(res)) {
        alert('エラー: ' + res);
    } else {
        alert('成功: ' + res);
    }
    return 'end';
}).then(function(res) {
    alert(res); // 'end'
});

このような、エラーなのか成功なのか不明な場合や 必ず実行したい場合に有用です。

isError (Pot.isError) は、 対象が Error オブジェクトかどうか判別します。 この例のような場合には特に有用です。

then, rescue, ensure それぞれの使用方法は同じです。 return した値が、次のコールバックの引数に使われます。

また、return した値が Pot.Deferred のインスタンスだった場合、 そのチェインの最終的な値が 次のコールバックの引数に渡されます。

begin(function() {
    return begin(function() {
        return 'ho';
    }).then(function(res) {
        var d = new Deferred();
        return d.then(function() {
            return res + 'ge';
        }).begin();
    });
}).then(function(res) {
    alert(res);
});

この例では 'hoge' とアラートされます。

Pot.Deferred を返す場合はネストが可能です。 Pot.Deferred を返し、そのコールバックでも Pot.Deferred を返す、 といったことが可能です。 最終的な結果 (return された) 値が次のコールバックの引数として渡されます。

分割代入

チェインのコールバック関数は、 引数の数を調節して結果値から分割代入 (Destructuring-Assignment) のようなことができます。

begin(function() {
    // 配列で返す
    return [1, 2, 3];
}).then(function(a, b, c) { // 結果の配列のアイテム数と引数の数を合わせる

    debug(a); // 1
    debug(b); // 2
    debug(c); // 3

    return [c, b, a]; // 逆順にして返す

}).then(function(a, b, c) {
    // 上と逆順になる

    debug(a); // 3
    debug(b); // 2
    debug(c); // 1

    return [c, b, a];

}).then(function(res) { // 通常通り引数一つで取得
    // 引数一つの時は分割代入されない

    debug(res); // [1, 2, 3]

});


速度の操作

Pot.Deferred は、コールバックチェインの速度を変更することができます。

var d = new Deferred();
// 速度を遅くする
d.speed('slow');

d.then(function() {
    console.log(1);
}).then(function() {
    console.log(2);
}).then(function() {
    console.log(3);
}).begin();

こうすることで、各コールバックはゆっくり実行されます。
この例では、console.log() が 1, 2, 3 とゆっくり表示されます。

速度の指定は、speed メソッドにより指定できます。 定義された値の文字列、または数値 (ms) で指定できます。 定義された文字列値は 下の表 を参照ください。

値 / メソッド名 速度
limp 最も遅い
doze 遅い
slow 遅め
normal 通常
fast 速め
rapid 速い
ninja 最も速い

または、コンストラクタに引数のオプションとして渡すことができます。

var d = new Deferred({ speed : 'slow' });

コールバックチェインの途中で速度を変更することも可能です。

var d = new Deferred();
d.then(function() {
    console.log(1);
}).speed('slow').then(function() {
    console.log(2);
    return 2 + 1;
}).speed(5000).then(function(res) {
    console.log(res); // 3
}).begin();

上の例のように、コールバック関数を用いないメソッドは 結果の値を引き継ぎ、次のコールバックに渡します。
したがって、speed()wait() などを使用しても 次のコールバックに渡す引数の値が失われることはありません。

何らかの重い処理を分割して処理する場合、speed の指定により負荷を軽減できます。

begin(function() {
    return someHeavyProcess();
}).speed('slow').then(function(res) {
    return moreHeavyProcess(res);
}).speed('doze').then(function(res) {
    return mostHeavyProcess(res);
}).speed('normal').then(function(res) {
    alert(res);
});

途中で wait を置くことも可能です。

begin(function() {
    return someHeavyProcess();
}).speed('slow').wait(1).then(function(res) {
    return moreHeavyProcess(res);
}).speed('doze').wait(2).then(function(res) {
    return mostHeavyProcess(res);
}).speed('normal').then(function(res) {
    alert(res);
});

wait は、秒数を引数に設定します。 ミリ秒ではないので注意してください。

チェインを実行

今までの例の途中でもいくつか出ましたが、
.begin() メソッドによりチェインを開始することができます。

関数としての begin (Pot.Deferred.begin) は、 Deferred のインスタンスを生成し、 チェインを開始するショートカットのようなものです。 別ものなので注意してください。

var d = new Deferred();
d.then(function() {
    alert('hoge');
});

この例は、何も実行されません。 'hoge' とアラートもされません。
単に変数 d にコールバックを登録しただけです。

これを実行するには、

d.begin();

とすると、コールバックチェインが開始されます。

また、引数に値を設定することができます。

var d = new Deferred();
d.then(function(value) {
    alert(value);
}).begin('hoge');

この例では、hoge とアラートされます。

次 (この場合は最初) のコールバックの引数の値として渡すことができます。

また、何らかの事態により .begin を通常処理ではなくエラーと扱いたい場合は、

var d = new Deferred();
d.then(function() {
    // 成功時
    successFunc();
}).rescue(function() {
    // エラー時
    errorFunc();
}).ensure(function() {
    // 最終的な処理
    finallyFunc();
});

// チェックが通れば成功とする場合
if (check()) {
    d.begin();
} else {
    d.raise();
}

このような分岐が必要な場合、 .begin() のかわりに .raise() を実行することで エラー扱いでコールバックチェインを開始することができます。
エラーから始まるので、最初に then があった場合は そのコールバックは無視されます。

.raise() も同様に、引数に何らかの値を与え 次のエラーバック関数の引数として渡すことができます。

関数を Deferred 化

既存の関数や、ユーザー定義の関数を Deferred 化、 つまり Pot.Deferred のインスタンスが返るよう置き換えたり 自分で作成する必要がでてくるかもしれません。

例として、XMLHttpRequest を使った非同期処理を書いてみます。

function request(url, options) {
    var deferred = new Deferred();
    var xhr = new XMLHttpRequest();
    if (options.queryString) {
        url += options.queryString;
    }
    xhr.open(options.method, url, true);
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                deferred.begin(xhr);
            } else {
                deferred.raise(xhr);
            }
        }
    };
    deferred.canceller(function() {
        xhr.abort();
    });
    xhr.send(options.sendContent || null);
    return deferred;
}

request 関数は、上のほうで例としてあげたものですが、 Pot.js および PotLite.js 内にも定義されています。
この例は、それを簡略化したものです。

Deferred を返すには、new Deferred としたものや、 Pot.Deferred.begin() のような あらかじめ定義されていて Deferred を返す関数を利用する必要があります。

接続が成功したら begin() を、失敗したら raise() をコールしています。
関数を定義する場合、エラー処理を明示してあげることで より適切な処理になります。

また、canceller() によりキャンセル処理を追加しています。
Deferred のキャンセルは、

deferred.cancel();

とすることで任意にコールバックチェインの実行を中止 または中断できます。

Pot.Deferred では、キャンセル処理としてのコールバック関数を スタッカブルに保持します。
なので、canceller() メソッドにより複数のコールバックが登録できます。

Pot.js および PotLite.js では、request 関数 のほかに jsonp 関数 も定義されています。

jsonp 関数 は、そのままの意味で JSONP によるデータ取得を行います。

使用方法、引数などは request, jsonp 共にほぼ同じで 返り値に XHR を設定された Deferred を返します。


他にも関数を Deferred 化するための方法があり、
deferrize (Pot.Deferred.deferrize) という関数が Pot.Deferred オブジェクトに定義されています。

// setTimeout を Deferred 化する
var timer = deferrize(window, 'setTimeout');

// setTimeout と同じ引数
timer(function() {
    debug(1);
}, 5000).then(function() {
    debug(2);
});

上の例は、setTimeout を Deferred 化し、それを実行します。

5 秒待ってから、 debug 関数によりコンソールに 1, 2 と出力されます。

deferrize はコールバック関数を引数とする場合は、 その関数の処理が終わった後に Deferred を開始し、 関数を引数として扱わないものは 処理が全て終わったあとに Deferred を開始します。 途中でエラーが起きると raise() により開始されます。
したがって、処理自体を置き換えたい場合でなければ 大抵の関数は deferrize で Deferred 化できます。

もっと複雑な置き換えをする場合や、 自前で Deferred 関数を作成したりする場合、 new Deferred や Pot.Deferred オブジェクトの関数などを使用して より細かな処理を独自に作成することができます。

イテレータ

Pot.js は、MochiKit ライクな実装の Deferred を中心に 非同期のループ、イテレートを重視して作られています。
(ここで言う MochiKit ライクとは、 コールバックチェインがひとつのインスタンスであるということです。)

また、JSDeferred の概要を強く尊重しています。

JSDeferred 紹介 より引用:

JavaScript における「高速化」

JavaScript における「高速化」では、単純に処理速度の高速化というよりは、 ユーザ体験のストレスをいかになくすかがとても重要です。

いくつにもネストした JavaScript のループ (for, for-in, while など) は、
特に Web ブラウザ上で実行される場合 処理が重たくなりがちです。

そこで、Pot.js は、非同期処理を直列的に書けるようにする Deferred を活用し、
forEach, map, reduce, filter, some, every, repeat, forEver などといったイテレート関数と結びつけ、
各ループ間の処理の重さを計算し、 自動的に CPU 負荷が軽減するよう調整しています。
そして、更なるストレス軽減を目標としています。

イテレートは、同期または非同期、Deferred チェイン上などで実行が可能です。

同期でのシンプルなオブジェクトのイテレートの場合、for-in では

for (var key in object) {...}

となりますが、Pot.js の forEach を使うと、

forEach(object, function(value, key) {...});

となります。
非同期でループする場合は、

Deferred.forEach(object, function(value, key) {...})

となり、Pot.Deferred のインスタンスを返します。 Pot.globalize() してない場合は Pot.Deferred.forEach です。

Deferred チェイン上で実行する場合も同じ扱いですが、引数が異なります。

(new Deferred()).forEach(function(value, key) {...}).begin( object );

各ループは、コールバック関数の処理の重さにより負荷がかからないよう 自動調節します。
この処理をすることで、例えば 10000 程の多くのアイテム数を持つ配列に対して for でループした場合と Deferred.forEach などのイテレータで実行した場合、
全体の処理の重さは大きな差が出ます。
また、それにより Web ブラウザによって発行される、
「処理が重くなっています、中断しますか?」 などのメッセージを回避することもできます。

Deferred に関連する イテレータのすべては、速度を調整することができます。
各ループをゆっくり実行したい場合は、

Deferred.forEach.slow(obj, function() {...});

逆に速くしたい場合は、

Deferred.forEach.fast(obj, function() {...});

などの指定が可能です。
他には 遅い順に limp, doze, slow, normal, fast, rapid, ninja が指定可能です。 速度の指定値については、上の速度の表 を参照ください。
その場に応じて使い分けができます。

時間のかかるループだけでなく、
瞬間的に負荷がかかり Web ブラウザが頻発して一瞬だけ固まる、といった事例もあげられます。
そのような処理も、1 回で済まそうとしないで分割することで解消できます。

例えば DOM 要素を大量に扱って操作する場合、

// ものすごい巨大なページだったとします。
var elems = document.getElementsByTagName('*');

従来通りループする場合、

var len = elems.length;
for (var i = 0; i < len; i++) {
    var elem = elems[i];
    someHeavyProcess(elem); // 何らかの重い処理
}

この方法では休む間を与えられず、UI が固まっていても容赦なく処理が続行されてしまいます。

これを Pot.Deferred を使って書くと、

Deferred.repeat(elems.length, function(i) {
    var elem = elems[i];
    someHeavyProcess(elem); // 何らかの重い処理
});

一見あまり変化なさそうですが、 1 ループごとに処理の負荷を計算して ある程度重いと判断した場合は Web ブラウザに制御を返し、 UI を独占しないよう調節します。

または、forEach を使うこともできます。

Deferred.forEach(elems, function(elem) {
    someHeavyProcess(elem); // 何らかの重い処理
});

someHeavyProcess() と 1 つの関数だけで表現しましたが、
これが、someHeavyProcess1, someHeavyProcess2, ... と いくつかの関数で構成されている場合、

Deferred.forEach(elems, function(elem) {
    return begin(function() {
        someHeavyProcess1(elem);
    }).wait(1).then(function() {
        someHeavyProcess2(elem);
    }).wait(1).then(function() {
        someHeavyProcess3(elem);
    });
});

このように分割し、任意に wait などを入れることが可能です。

なお、begin (Pot.Deferred.begin) は、 Pot.Deferred オブジェクトのインスタンスを返すので Deferred.forEach などの中で実行する場合、 return が必要です。 return しないと整合性がとれなくなってしまいます。

それでも重い場合、

Deferred.forEach.slow(elems, function(elem) {
    someHeavyProcess(elem);
});

ゆっくり実行することを明示し slow を指定して速度を調整できます。

slow の次に遅い doze にすると、必ず毎回のループごとに Web ブラウザに制御を返します。
結果として処理時間は長くなりますが、 途中で固まったりカクカクするようなことが避けられます。

速度指定は 'メソッド.速度()' の形式で利用できます。
例えば、 'forEach.rapid(...)' のような指定です。
forEach, repeat, map, filter など、すべてのイテレータで指定可能です。

定義された速度の値は、上の速度の表 を参照してください。

速度指定の表にある 「最も速い」 という表記は、
逆に言うと 最も負荷をかけやすいとも言えます。
とはいっても for-in などでループすることと比べたら UI への配慮は少なからず存在するという差はあります。

大抵は自ら速度調整する必要はありませんが、 特殊な用途やデバッグ時など、必要になることもあります。

ループ処理を置き換える

既存のループで負荷がかかっている処理を、 Pot.js のイテレータを用いて軽減させることができます。

function someLoop(n, c) {
    var results = [];
    for (var i = 0; i < n; i++) {
        var array = [];
        for (var j = 0; j < c; j++) {
            array[j] = j;
        }
        results[i] = array;
    }
    return results;
}

例えばこのような関数があったとします。
引数 c 回ループした配列を引数 n 個分持つ配列を返す関数です。

例えばこれを、

var array = someLoop(100000, 1000);

このような大きい配列を作るとなると、 スペックにもよりますが、 瞬間的な負荷は大きくなり UI を独占してしまうでしょう。

この関数を Deferred 化してみます。

function someLoopDefer(n, c) {
    var results = [];
    return Deferred.repeat(n, function(i) {
        var array = [];
        for (var j = 0; j < c; j++) {
            array[j] = j;
        }
        results[i] = array;
    }).then(function() {
        return results;
    });
}

例として、このようになります。
内側の for はこのままでいいのかと思うかもしれませんが、 おそらく repeatDeferred.repeat 等に変えたところで 必要以上に負荷が分散され、結果としてかなり速度は落ちてしまうでしょう。
なので、いちばん外側のループのみを置き換えます。
もっとも、この例の場合は 内側の for を何度も実行する必要はありませんが、 あくまで例として捉えてください。
返り値は Deferred になります。

someLoopDefer(100000, 1000).then(function(res) {
    var array = res;
});

Deferred が返るので、then などで結果を取得します。 そのままチェインを繋げることもできます。

この変化により、瞬間的な処理の重さが分散され 安定した処理が可能となります。
そのかわり、処理時間は若干伸びる可能性があります。

Pot.Deferred のイテレータを使う場合、 このように あまり意識せずに分散的なループが可能となります。

イテレートを止める

Pot.js, PotLite.js で実装されている すべてのイテレータは、
StopIteration を throw することで中断できます。

var foo = '';
forEver(function(i) {
    foo += 'foo';
    if (foo.length > 10) {
        throw StopIteration;
    }
});

debug(foo);

この例は、debug 関数により コンソールに 'foofoofoofoo' と出力されます。

forEver (Pot.forEver) は、StopIteration が throw されるまで 永遠にループします。
複雑な条件式でループを制御する場合や、 そのような既存の重い処理を Deferred.forEver で置き換えたい場合などに活用できます。

StopIteration は、 Array.prototype.forEach が有効な環境など、 コンフリクトする可能性があるときは Pot.StopIteration と明示することで解消できます。

StopIteration を判別する方法は、

if (isStopIter(e)) {...}

または、

if (e == StopIteration) {...}

と単純に比較か、

if (e instanceof StopIteration) {...}

とすることもできます。

おそらく Pot.js ライブラリが実装している isStopIter (Pot.isStopIter) が確実です。


実装されているすべてのイテレータは、 コールバック関数が非同期の Deferred オブジェクトを いくつにもネストしたとしても、
throw StopIteration によりスコープ間を超えて 実行中のイテレータ関数のスコープまで届くよう設計されています。

var result = [];
begin(function() {
    return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}).forEach(function(value) {
    return begin(function() {
        var d = new Deferred();
        return d.then(function() {
            if (value > 5) {

                // 実行しているイテレータのスコープが対象になるため
                // この例では いちばん外側の .forEach まで届く

                throw StopIteration;
            }
            return value * 100;
        }).begin();
    }).then(function(res) {
        result.push(res);
    });
}).then(function() {
    debug(result);
    //  =>  [100, 200, 300, 400, 500]
});