jqueryのDeferredの使用と実現

33701 ワード


オブザーバーモードは開発でよく用いられるモードであり,このモードは2つの主要な部分から構成されている:テーマとオブザーバー.観察者モードにより,主題と観察者のデカップリングを実現した.
トピックはコンテンツのパブリケーションを担当し、オブザーバーはトピックのパブリケーションのコンテンツを受信します.通常、観察者は複数であるため、すべての観察者を保存するために1つの集合が必要であり、トピックがコンテンツをパブリッシュした後、トピックがパブリッシュされたコンテンツを順番に観察者に提供し、プログラムの観点から、観察者は多くの方法であり、コンテンツをパラメータとして順番にこれらの方法を呼び出す.
前のjQueryのCallbacksを見たことがあるなら、Callbacksで観察者のリストを管理するのは便利だと思います.もう一つのテーマを追加すると、観察者モードを実現することができます.jQueryでは、このモードがあちこちで使用されています.ready関数を使用したことがないとは言わないでください.思い出すと、1ページでready関数を何度も使用することができます.readyイベントがトリガーされた後、これらの関数は順次呼び出されます.このメカニズムは簡単なイベント処理メカニズムを突破しています.
なお、readyイベント、ajaxのリクエスト処理など、多くのイベントが一度だけトリガーされる場合、Deferredを使用すると便利です.
Deferredの使用
jQueryでは,観察者モードを実現するのがDeferredであるが,まずその使用を見る.jQueryのDeferredドキュメントも直接見ることができます.
このオブジェクトは、トピックとサブスクリプションの管理を提供し、使い捨てのオブザーバモードを容易に実現します.
//     
var subject = (function(){
    var dfd = $.Deferred();
    
    return dfd;
})();

//      
var fn1 = function(content){
    console.log("fn1: " + content );
}

var fn2 = function(content){
    console.log("fn2: " + content );
}

//      
$.when( subject )
.done( fn1 )
.done( fn2 );

//     
subject.resolve("Alice");

通常、トピックの内部では、トピックの外でパブリッシュすることを許可せずに、いつ、どのコンテンツをパブリッシュするかを決定します.Deferredオブジェクトのpromiseメソッドにより、トピックの外に観察者を登録することを許可することができ、少し似ています.NETでイベントの処理をしました.これにより、私たちのコードは次の形式になります.
//     
var subject = (function(){
    var dfd = $.Deferred();
    
    var task = function()
    {
        //     
        dfd.resolve("Alice");
    }
    
    setTimeout( task, 3000);
    
    return dfd.promise();
})();

//      
var fn1 = function(content){
    console.log("fn1: " + content );
}

var fn2 = function(content){
    console.log("fn2: " + content );
}

//      
$.when( subject )
.done( fn1 )
.done( fn2 );

jQueryでは、2つのトピックが同時に観察されるようにすることもできますが、2つのトピックがトリガーされてから、本当にトリガーされるのを待つ必要があります.各オブジェクトは一度に2つのトピックを得るので、パラメータは2つになります.
//     
var subjectAlice = (function(){
    var dfd = $.Deferred();
    
    var task = function()
    {
        //     
        dfd.resolve("Alice");
    }
    
    setTimeout( task, 3000);
    
    return dfd.promise();
})();

var subjectTom = (function(){
    var dfd = $.Deferred();
    
    var task = function()
    {
        //     
        dfd.resolve("Tom");
    }
    
    setTimeout( task, 1000);
    
    return dfd.promise();
})();

//      
var fn1 = function(content1, content2){
    console.log("fn1: " + content1 );
    console.log("fn1: " + content2 );
}

var fn2 = function(content1, content2){
    console.log("fn2: " + content1 );
    console.log("fn2: " + content2 );
}

//      
$.when( subjectAlice, subjectTom )
.done( fn1 )
.done( fn2 );

実際、jQueryでは、正常に完了したイベントだけでなく、トピックでも失敗と処理の2つのイベントをパブリッシュできます.
失敗イベントは,トピックのrejectメソッドを呼び出すことで失敗したメッセージをパブリッシュすることができ,観察者にとってfailでこのイベントを登録する必要がある.
処理中のイベントは,トピックのnotifyを呼び出して処理中のメッセージをパブリッシュし,観察者にとってprogressによってこのイベントを登録する必要がある.
観察者が複数のイベントを一度に登録したい場合は、thenによって登録することができ、この方法でトピックの成功、失敗、および処理の3つのイベントを処理することができる.
$.get( "test.php" ).then(
  function() {
    alert( "$.get succeeded" );
  }, function() {
    alert( "$.get failed!" );
  }
);

成功と失敗だけを考えて、alwaysで処理します.
$.get( "test.php" )
.always(function() { alert( "$.get completed with success or error callback arguments" ); });

 
jQueryでのDeferredの使用
よく使われるのはajax、get、postなどのAjax関数です.それらの内部はすでにDeferredとして実現されており,返される結果はDeferredオブジェクトである.つまり、ajaxが成功した後にresolveを呼び出してdoneイベントをトリガーし、パラメータを渡す問題など、オブジェクトを書くだけでいいのです.従来のコールバック方式を引き続き使用することができ、明らかにDeferred方式をお勧めします.これにより、コード構造がより明確になります.
$.post( "test.php", { name: "John", time: "2pm" })
  .done(function( data ) {
    alert( "Data Loaded: " + data );
  });

2つのAjaxにアクセスする必要がある場合は、次のようにします.
$.when( $.post( "test.php", { name: "John", time: "2pm" }),
        $.post( "other.php" ) )
  .done(function( data1, data2 ) {
    alert( "Data Loaded: " + data1 );
    alert( "Data Loaded: " + data2 );
  });

 
Deferredの実装
前の使用では、Deferredというオブジェクトの内部には、3つのコールバックキューが必要であり、ここでの成功と失敗は一度にしかできないので、この2つのCallbacksはonceを使用して定義されています.
var tuples = [
        // action, add listener, listener list, final state
        [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
        [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
        [ "notify", "progress", jQuery.Callbacks("memory") ]
            ],

現在の処理のステータス.
state = "pending",
promise = {
    state: function() {
        return state;
    },

Alwaysは2つのイベントを直接登録した.thenでは、3つの登録を一度に処理できます.
always: function() {
        deferred.done( arguments ).fail( arguments );
        return this;
    },
    then: function( /* fnDone, fnFail, fnProgress */ ) {
        var fns = arguments;
        return jQuery.Deferred(function( newDefer ) {
            jQuery.each( tuples, function( i, tuple ) {
                var action = tuple[ 0 ],
                    fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
                // deferred[ done | fail | progress ] for forwarding actions to newDefer
                deferred[ tuple[1] ](function() {
                    var returned = fn && fn.apply( this, arguments );
                    if ( returned && jQuery.isFunction( returned.promise ) ) {
                        returned.promise()
                            .done( newDefer.resolve )
                            .fail( newDefer.reject )
                            .progress( newDefer.notify );
                    } else {
                        newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
                    }
                });
            });
            fns = null;
        }).promise();
    },

deferredはオブジェクトです.pipeはもう時代遅れの使い方で、thenの別名です.
deferred = {};

    // Keep pipe for back-compat
    promise.pipe = promise.then;

    // Add list-specific methods
    jQuery.each( tuples, function( i, tuple ) {
        var list = tuple[ 2 ],
            stateString = tuple[ 3 ];

        // promise[ done | fail | progress ] = list.add
        promise[ tuple[1] ] = list.add;

        // Handle state
        if ( stateString ) {
            list.add(function() {
                // state = [ resolved | rejected ]
                state = stateString;

            // [ reject_list | resolve_list ].disable; progress_list.lock
            }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
        }

        // deferred[ resolve | reject | notify ]
        deferred[ tuple[0] ] = function() {
            deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
            return this;
        };
        deferred[ tuple[0] + "With" ] = list.fireWith;
    });

    // Make the deferred a promise
    promise.promise( deferred );

    // Call given func if any
    if ( func ) {
        func.call( deferred, deferred );
    }

    // All done!
    return deferred;
},

whenはアシスタントメソッドで、複数のトピックをサポートします.
// Deferred helper
when: function( subordinate /* , ..., subordinateN */ ) {
    var i = 0,
        resolveValues = core_slice.call( arguments ),
        length = resolveValues.length,

        // the count of uncompleted subordinates
        remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,

        // the master Deferred. If resolveValues consist of only a single Deferred, just use that.
        deferred = remaining === 1 ? subordinate : jQuery.Deferred(),

        // Update function for both resolve and progress values
        updateFunc = function( i, contexts, values ) {
            return function( value ) {
                contexts[ i ] = this;
                values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;
                if( values === progressValues ) {
                    deferred.notifyWith( contexts, values );
                } else if ( !( --remaining ) ) {
                    deferred.resolveWith( contexts, values );
                }
            };
        },

        progressValues, progressContexts, resolveContexts;

    // add listeners to Deferred subordinates; treat others as resolved
    if ( length > 1 ) {
        progressValues = new Array( length );
        progressContexts = new Array( length );
        resolveContexts = new Array( length );
        for ( ; i < length; i++ ) {
            if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
                resolveValues[ i ].promise()
                    .done( updateFunc( i, resolveContexts, resolveValues ) )
                    .fail( deferred.reject )
                    .progress( updateFunc( i, progressContexts, progressValues ) );
            } else {
                --remaining;
            }
        }
    }

    // if we're not waiting on anything, resolve the master
    if ( !remaining ) {
        deferred.resolveWith( resolveContexts, resolveValues );
    }

    return deferred.promise();
}

完全なコードは次のとおりです.
jQuery.extend({

    Deferred: function( func ) {
        var tuples = [
                // action, add listener, listener list, final state
                [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
                [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
                [ "notify", "progress", jQuery.Callbacks("memory") ]
            ],
            state = "pending",
            promise = {
                state: function() {
                    return state;
                },
                always: function() {
                    deferred.done( arguments ).fail( arguments );
                    return this;
                },
                then: function( /* fnDone, fnFail, fnProgress */ ) {
                    var fns = arguments;
                    return jQuery.Deferred(function( newDefer ) {
                        jQuery.each( tuples, function( i, tuple ) {
                            var action = tuple[ 0 ],
                                fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
                            // deferred[ done | fail | progress ] for forwarding actions to newDefer
                            deferred[ tuple[1] ](function() {
                                var returned = fn && fn.apply( this, arguments );
                                if ( returned && jQuery.isFunction( returned.promise ) ) {
                                    returned.promise()
                                        .done( newDefer.resolve )
                                        .fail( newDefer.reject )
                                        .progress( newDefer.notify );
                                } else {
                                    newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
                                }
                            });
                        });
                        fns = null;
                    }).promise();
                },
                // Get a promise for this deferred
                // If obj is provided, the promise aspect is added to the object
                promise: function( obj ) {
                    return obj != null ? jQuery.extend( obj, promise ) : promise;
                }
            },
            deferred = {};

        // Keep pipe for back-compat
        promise.pipe = promise.then;

        // Add list-specific methods
        jQuery.each( tuples, function( i, tuple ) {
            var list = tuple[ 2 ],
                stateString = tuple[ 3 ];

            // promise[ done | fail | progress ] = list.add
            promise[ tuple[1] ] = list.add;

            // Handle state
            if ( stateString ) {
                list.add(function() {
                    // state = [ resolved | rejected ]
                    state = stateString;

                // [ reject_list | resolve_list ].disable; progress_list.lock
                }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
            }

            // deferred[ resolve | reject | notify ]
            deferred[ tuple[0] ] = function() {
                deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
                return this;
            };
            deferred[ tuple[0] + "With" ] = list.fireWith;
        });

        // Make the deferred a promise
        promise.promise( deferred );

        // Call given func if any
        if ( func ) {
            func.call( deferred, deferred );
        }

        // All done!
        return deferred;
    },

    // Deferred helper
    when: function( subordinate /* , ..., subordinateN */ ) {
        var i = 0,
            resolveValues = core_slice.call( arguments ),
            length = resolveValues.length,

            // the count of uncompleted subordinates
            remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,

            // the master Deferred. If resolveValues consist of only a single Deferred, just use that.
            deferred = remaining === 1 ? subordinate : jQuery.Deferred(),

            // Update function for both resolve and progress values
            updateFunc = function( i, contexts, values ) {
                return function( value ) {
                    contexts[ i ] = this;
                    values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;
                    if( values === progressValues ) {
                        deferred.notifyWith( contexts, values );
                    } else if ( !( --remaining ) ) {
                        deferred.resolveWith( contexts, values );
                    }
                };
            },

            progressValues, progressContexts, resolveContexts;

        // add listeners to Deferred subordinates; treat others as resolved
        if ( length > 1 ) {
            progressValues = new Array( length );
            progressContexts = new Array( length );
            resolveContexts = new Array( length );
            for ( ; i < length; i++ ) {
                if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
                    resolveValues[ i ].promise()
                        .done( updateFunc( i, resolveContexts, resolveValues ) )
                        .fail( deferred.reject )
                        .progress( updateFunc( i, progressContexts, progressValues ) );
                } else {
                    --remaining;
                }
            }
        }

        // if we're not waiting on anything, resolve the master
        if ( !remaining ) {
            deferred.resolveWith( resolveContexts, resolveValues );
        }

        return deferred.promise();
    }
});