Deferred を作ろう!
みなさんこんにちは!
閃光部おばらです。
ajax やアニメーション等の非同期処理を行う際、Deferred という概念を用いると非常に作りやすくなります。
Deferred の機能を提供するライブラリは多々ありますが、
今回は必要最低限の機能を持つ Deferred をフルスクラッチしてみましょう。
仕様
今回作成する Deferred の仕様は以下の通りとします。
・【待機中】と【解決】の二つの内部状態を持ち得る。
・初期状態は【待機中】。
・ユーザーは(メソッドを呼び出すことにより)内部の状態を【待機中】から【解決】に一度だけ変更可能。
・一度、内部状態を【解決】に変更した後は、他の状態に変更不可。
・ユーザーは(メソッドを呼び出すことにより)任意の数のコールバック関数を登録(予約)できる。
・登録時の内部状態が【待機中】の場合、コールバック関数はキューに追加される。
(【待機中】から【解決】に変更されたら、キュー内のコールバック関数が順番に実行される。)
・登録時の内部状態が【解決】の場合、コールバック関数は即座に実行される。
インターフェイス
インターフェイスは jQuery の $.Deferred を参考に以下のようにします。
// インスタンスを生成
var deferred = new Deferred;
// コールバックを登録
deferred.done(function() {
console.log("foo");
});
// コールバックを実行
deferred.resolve();
// コールバックを登録&即実行
deferred.done(function() {
console.log("bar");
});
まずクラスベースっぽくするため new でインスタンスを生成できるようにします。
また簡単のため、メソッドは resolve() と done() のみとします。
多くの場合この二つで十分でしょう。
実装
それでは早速実装していきましょう。
prototypeを使用すると複雑になる&プライベートな変数を実現しづらいため、
多少のメモリ効率の悪さには目をつぶり、モジュールパターンで実装します。
なお、メソッドパターンの場合、実際には new する必要はないのですが、
わかりやすさのため new でインスタンスを生成するものとします。
まずはコンストラクタ関数(にあたるもの)から
function Deferred() {
;
}
次にパブリックなメソッド resolve() と done() を定義します。
/**
*
*/
function Deferred() {
/*-------------------------------------------
PUBLIC
-------------------------------------------*/
function resolve() {
;
}
function done() {
;
}
}
パブリックなメソッド done() と resolve() は ユーザーが呼び出せるように return でエクスポートしましょう。
/**
* @return {Object.<function>}
*/
function Deferred() {
/*-------------------------------------------
PUBLIC
-------------------------------------------*/
function resolve() {
;
}
function done() {
;
}
/*-------------------------------------------
EXPORT
-------------------------------------------*/
return {
resolve : resolve,
done : done
};
}
次にプライベート変数を作成します。
最低限必要なのは、
1) 内部状態を格納する変数(フラグ) _isResolved と、
2) コールバックを格納する配列(キュー) _queue です。
_isResolve の初期値は false とします。
_queue の初期値は空の配列とします。
※プライベートな変数、関数の識別子は慣習に基づき _ で始めるものとします。
/**
* @return {Object.<function>}
*/
function Deferred() {
/*-------------------------------------------
PRIVATE
-------------------------------------------*/
var _isResolved = false,
_queue = [];
/*-------------------------------------------
PUBLIC
-------------------------------------------*/
function resolve() {
;
}
function done() {
;
}
/*-------------------------------------------
EXPORT
-------------------------------------------*/
return {
resolve : resolve,
done : done
};
}
さて、これで最低限必要な変数、メソッドが用意できました。
ここからいよいよメソッドの実装に入ります。
resolve()
resolve() で行う処理は以下の二つです。
1)プライベート変数 _isResolved を true に変更する。
2)キューを消化する。
/**
* @return {undefined}
*/
function resolve() {
var len = _queue.length,
i = 0;
_isResolved = true;
for (; i < len; ++i) {
_queue[i]();
}
}
※簡単のため、キューの中身が関数かどうかといった型チェックは省いています。
なお今回は気にしなくてOKですが、
キューをループで回す際にループ中に(ユーザーによって)キューが変更される可能性がある場合は
以下のようにキューの浅いコピーを作成し使用すると良いでしょう。
var copy = [].concat(_queue),
len = copy.length,
i = 0;
for (; i < len; ++i) {
// do something...
}
done()
done() で行う処理は _isResolved の値により、異なります。
1) _isResolved === true の場合
コールバック関数を即、実行する。
2) _isResolved === false の場合
_queueにコールバック関数を追加する。
実装は以下のようになります。
/**
* @param {function} func
* @return {undefined}
*/
function done(func) {
if (_isResolved === true) {
func();
} else if (_isResolved === false) {
_queue.push(func);
}
}
もう少し簡潔に書くと...
/**
* @param {function} func
* @return {undefined}
*/
function done(func) {
_isResolved ? func() : _queue.push(func);
}
ここまでをまとめると、以下のようになります。
/**
* @return {Object.<function>}
*/
function Deferred() {
/*-------------------------------------------
PRIVATE
-------------------------------------------*/
var _isResolved = false,
_queue = [];
/*-------------------------------------------
PUBLIC
-------------------------------------------*/
/**
* @return {undefined}
*/
function resolve() {
var len = _queue.length,
i = 0;
_isResolved = true;
for (; i < len; ++i) {
_queue[i]();
}
}
/**
* @param {function} func
* @return {undefined}
*/
function done(func) {
_isResolved ? func() : _queue.push(func);
}
/*-------------------------------------------
EXPORT
-------------------------------------------*/
return {
resolve : resolve,
done : done
};
}
ここでもう少し簡潔に書けないか考察してみましょう。
_isResolved と _queue の関係に注目します。
_queueが必要となるのは _isResolved が false の場合のみです。
また、_isResolved は一度 true になると、再度 false に戻ることはありません。
(その手段がありません)
そこで _isResolved を無くし、
代わりに変数 _queue を内部状態を示すフラグとしても使うことにしましょう。
/**
* @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
};
}
これで完成です。
このように Deferred の基本的な機能はわずか(コメントを除くと)20行足らずで実装可能です。
使用例
最後に、今回作成した Deferred を用い、CSSアニメーション用のユーティリティ関数を作成してみましょう。
※WebKitのみ
/**
* @param {string} cssSelector
* @return {Object.<function>}
*/
function $(cssSelector) {
/*-------------------------------------------
PRIVATE
-------------------------------------------*/
var _element = document.querySelector(cssSelector),
_deferred = new Deferred;
/*-------------------------------------------
INIT
-------------------------------------------*/
_deferred.resolve();
/*-------------------------------------------
PRIVATE
-------------------------------------------*/
/**
* @private
* @param {Deferred} prevDeferred
* @param {number} x
* @param {number} y
* @return {Object.<function>}
*/
function _to(prevDeferred, x, y) {
var nextDeferred = new Deferred;
prevDeferred.done(render);
function handleEnd() {
_element.removeEventListener("webkitTransitionEnd", handleEnd, false);
nextDeferred.resolve();
}
function render() {
_element.addEventListener("webkitTransitionEnd", handleEnd, false);
_element.offsetTop;// force a reflow
_element.style.cssText += ";" +
"-webkit-transform-style: preserve-3d;" + // switch to 3d
"-webkit-transition: -webkit-transform .5s ease-in-out;" + // enable transition
"-webkit-transform: translate(" + x + "px," + y + "px);";
}
function to(x, y) {
return _to(nextDeferred, x, y);
}
return {
to : to,
done : nextDeferred.done
};
}
/*-------------------------------------------
PUBLIC
-------------------------------------------*/
/**
* @param {number} x
* @param {number} y
* @return {Object.<function>}
*/
function to(x, y) {
return _to(_deferred, x, y);
}
/*-------------------------------------------
EXPORT
-------------------------------------------*/
return {
to : to,
done : _deferred.done
};
}
これを Deferred 無しで作るとしたら。。。ゾッとしますね><
まさに、Deferred 様々です。
使用方法は以下の通りです。
$("#hoge").
to( 0, 100).
to(100, 100).
to(100, 200).
to(200, 200).
done(function() {
alert("オワタ\(^o^)/");
});
以下にサンプルコードを置きましたので、ご覧ください!
追記:続きはこちら!!→Deferred を作ろう!#2

