こんにちは!閃光部おばらです。

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

HTML5飯