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