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トランジション終了の検知