Deferred を作ろう!#2
こんにちは!閃光部おばらです。
前回は Deferred をフルスクラッチで作成しました。
今回はそれをさらに使いやすくしていきましょう。
※前回の記事はこちらです。併せてお読みください。
http://level0.kayac.com/#!2012/06/deferred.php
復習
以下が前回ご紹介したコードです。
/**
* @return {Object.<function>}
*/
function Deferred() {
/*-------------------------------------------
PRIVATE
-------------------------------------------*/
var _queue = [];
/*-------------------------------------------
PUBLIC
-------------------------------------------*/
/**
* @return {undefined}
*/
function resolve() {
var arr = _queue,
len = arr.length,
i = 0;
_queue = null;
for (; i < len; ++i) {
arr[i]();
}
}
/**
* @param {function} func
* @return {undefined}
*/
function done(func) {
_queue ? _queue.push(func) : func();
}
/*-------------------------------------------
EXPORT
-------------------------------------------*/
return {
resolve : resolve,
done : done
};
}
パブリックメソッドは resolve() と done() の二つ。
done() でコールバック関数を任意の数だけ登録し、resolve() でそれらを実行します。
前回の例:CSSアニメーションの場合は以上の仕様で十分でした。
しかし、非同期でサーバー上のファイルを取得するケースを考えると、
レスポンス内容を resolve() に引数として渡せたら便利ですね。
また、通信が【成功した場合】&【失敗した場合】の2種類のコールバック関数を追加できるとなお良いです。
では早速、修正していきましょう。
現在の状態を取得できるメソッドを用意する
後で使うため、現在の状態を真偽値で取得するメソッド isResolved() を追加しておきます。
/**
* @return {boolean}
*/
function isResolved() {
return !_queue;
}
resolve() が引数をとれるようにする
まずは(レスポンス内容などの)データを保存しておくプライベート変数 _data を定義します。
function Deferred() {
var _data;
.
.
.
}
resolve()がデータを引数として受け取れるよう、以下のように修正します。
/**
* @param {*} data
* @return {undefined}
*/
function resolve(data) {
var arr = _queue,
len = arr.length,
i = 0;
_queue = null;
_data = data;
for (; i < len; ++i) {
arr[i](data);
}
}
resolve() で受け取った引数 data の内容は、
resolve() 後、done() に渡されたコールバック関数にも渡す必要があるため、
以下のように引数 data をプライベート変数 _data に保存しています。
_data = data;
また、先程作成した isResolved() を使い、
resolve() が呼ばれた際に、すでに【解決】となっていないかをチェックする処理を加えておきましょう。
こうすることで、ユーザーが誤って resolve() を複数回呼んでしまった場合にも対応できます。
/**
* @param {*} data
* @return {undefined}
*/
function resolve(data) {
if (isResolved()) {
return;
}
var arr = _queue,
len = arr.length,
i = 0;
_queue = null;
_data = data;
for (; i < len; ++i) {
arr[i](data);
}
}
done() の修正
コールバック関数の実行時、
引数に _data を渡すよう修正します。
/**
* @param {function} func
* @return {undefined}
*/
function done(func) {
_queue ? _queue.push(func) : func(_data);
}
まとめると、以下となります。
/**
* @return {Object.<function>}
*/
function Deferred() {
/*-------------------------------------------
PRIVATE
-------------------------------------------*/
var _queue = [],
_data;
/*-------------------------------------------
PUBLIC
-------------------------------------------*/
/**
* @return {boolean}
*/
function isResolved() {
return !_queue;
}
/**
* @param {*} data
* @return {undefined}
*/
function resolve(data) {
if (isResolved()) {
return;
}
var arr = _queue,
len = arr.length,
i = 0;
_queue = null;
_data = data;
for (; i < len; ++i) {
arr[i](data);
}
}
/**
* @param {function} func
* @return {undefined}
*/
function done(func) {
_queue ? _queue.push(func) : func(_data);
}
/*-------------------------------------------
EXPORT
-------------------------------------------*/
return {
isResolved : isResolved,
resolve : resolve,
done : done
};
}
成功時/失敗時の対応
これまで、Deferred が持ち得る内部状態は以下の二つでした。
・【待機中】
・【解決】
ここにもう一つの状態【失敗】を加えましょう。
・【待機中】
・【解決】
・【失敗】
併せて以下の二つのメソッドを追加します。
・reject() // 内部状態を【待機中】から【失敗】に変更する
・fail() // 【失敗】時用のコールバック関数を追加する
インターフェイス
さて、いよいよ実装に入るのですが、
これまで作ってきた Deferred にまたさらにメソッドを加えると複雑になるので、
新たに別のクラス SuperDeferred を作成することにします。
Deferred は SuperDeferred の内部でいわゆる委譲の形で使用していきます。
まずはインターフェイスを完成させましょう。
/**
* @return {Object.<function>}
*/
function SuperDeferred() {
/*-------------------------------------------
PUBLIC
-------------------------------------------*/
function resolve() {
;
}
function reject() {
;
}
function done() {
;
}
function fail() {
;
}
/*-------------------------------------------
EXPORT
-------------------------------------------*/
return {
resolve : resolve,
reject : reject,
done : done,
fail : fail
};
}
実装
成功時と失敗時用の Deferred オブジェクトをそれぞれプライベート変数として定義します。
function SuperDeferred() {
var _dfdDone = new Deferred,
_dfdFail = new Deferred;
.
.
.
}
SuperDeferred::resolve() と SuperDeferred::reject() は、
それぞれ _dfdDone.resolve() と _dfdFail.resolve() を呼ぶ形にします。
※apply() の第一引数はこの場合実行結果に影響しないため null としておきます。
/**
* @param {*} data
* @return {undefined}
*/
function resolve(/* data */) {
_dfdDone.resolve.apply(null, arguments);
}
/**
* @param {*} data
* @return {undefined}
*/
function reject(/* data */) {
_dfdFail.resolve.apply(null, arguments);
}
一度【解決】になったら【失敗】にはならない、
また、その逆も同様であること保障するため、
現在の状態をチェックする処理を追加しましょう。
/**
* @param {*} data
* @return {undefined}
*/
function resolve(/* data */) {
if (_dfdFail.isResolved()) {
return;
}
_dfdDone.resolve.apply(null, arguments);
}
/**
* @param {*} data
* @return {undefined}
*/
function reject(/* data */) {
if (_dfdDone.isResolved()) {
return;
}
_dfdFail.resolve.apply(null, arguments);
}
また、SuperDeferred::done() と SuperDeferred::fail() も、
それぞれ _dfdDone.done() と _dfdFail.done() を呼ぶ形にします。
/**
* @param {function} func
* @return {undefined}
*/
function done(/* func */) {
_dfdDone.done.apply(null, arguments);
}
/**
* @param {function} func
* @return {undefined}
*/
function fail(/* func */) {
_dfdFail.done.apply(null, arguments);
}
メソッドチェーンで使用できるよう、return this; を加えておきましょう。
/**
* @param {function} func
* @return {SuperDeferred}
*/
function done(/* func */) {
_dfdDone.done.apply(null, arguments);
return this;
}
/**
* @param {function} func
* @return {SuperDeferred}
*/
function fail(/* func */) {
_dfdFail.done.apply(null, arguments);
return this;
}
まとめると以下となります。
/**
* @return {Object.<function>}
*/
function SuperDeferred() {
/*-------------------------------------------
PRIVATE
-------------------------------------------*/
var _dfdDone = new Deferred,
_dfdFail = new Deferred;
/*-------------------------------------------
PUBLIC
-------------------------------------------*/
/**
* @param {*} data
* @return {undefined}
*/
function resolve(/* data */) {
if (_dfdFail.isResolved()) {
return;
}
_dfdDone.resolve.apply(null, arguments);
}
/**
* @param {*} data
* @return {undefined}
*/
function reject(/* data */) {
if (_dfdDone.isResolved()) {
return;
}
_dfdFail.resolve.apply(null, arguments);
}
/**
* @param {function} func
* @return {SuperDeferred}
*/
function done(/* func */) {
_dfdDone.done.apply(null, arguments);
return this;
}
/**
* @param {function} func
* @return {SuperDeferred}
*/
function fail(/* func */) {
_dfdFail.done.apply(null, arguments);
return this;
}
/*-------------------------------------------
EXPORT
-------------------------------------------*/
return {
resolve : resolve,
reject : reject,
done : done,
fail : fail
};
}
サンプル
上で作成した、SuperDeferred を使用して、
非同期通信(GET)を行うユーティリティ関数を作成してみましょう。
/**
* @param {string} src
* @return {SuperDeferred}
*/
function ajax(src) {
var deferred = new SuperDeferred,
xhr = new XMLHttpRequest;
xhr.onreadystatechange = handleReadyStateChange;
xhr.open("GET", src, true);
xhr.send(null);
function handleReadyStateChange() {
var status;
if (+xhr.readyState === 4) {
status = +xhr.status;
if ((200 <= status && status < 300) || status === 304) {
deferred.resolve(xhr.responseText);
} else {
deferred.reject();
}
xhr = xhr.onreadystatechange = null;
}
}
return deferred;
}
使用方法は以下の通りです。
ajax("/hoge.txt").
done(function(data) {
alert("成功" + data);
}).
fail(function() {
alert("失敗");
});
ajax() の引数で成功時と失敗時のコールバックを受け取るよりも、
こちらの方がより柔軟性があり使いやすいですね。
サンプルコードを用意しましたので、ぜひご覧ください!
続きはこちら!!→Deferred を使おう:CSSトランジション終了の検知

