• jquery 之 Deferred 使用与实现


    观察者模式是开发中经常使用的模式,这个模式由两个主要部分组成:主题和观察者。通过观察者模式,实现主题和观察者的解耦。

    主题负责发布内容,而观察者则接收主题发布的内容。通常情况下,观察者都是多个,所以,我们需要一个集合来保存所有的观察者,在主题发布内容之后,依次将主题发布的内容提供给观察者,从程序的角度来说,观察者就是一堆的方法,我们将内容作为参数依次调用这些方法。

    如果你已经看过上一篇 jQuery 之 Callbacks, 那么,你会发现,通过 Callbacks 来管理观察者的列表是很方便的事情。再添加一个主题,我们就可以实现观察者模式了。在 jQuery 中,这个模式被到处使用,不要说你没有使用过 ready 函数,回想一下,你可以在一个页面中,多次使用 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 中 event 的处理了。这样,我们的代码就成为下面的形式。

    // 定义主题
    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 个。

    // 定义主题
    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 中,不仅可以发布成功完成的事件,主题还可以发布其它两种事件:失败和处理中。

    失败事件,通过调用主题的 reject 方法可以发布失败的消息,对于观察者来说,需要通过 fail 来注册这个事件了。

    处理中事件,通过调用主题的 notify 来发布处理中的消息,对于观察者来说,需要通过 progress 来注册这个事件。

    要是观察者想一次性注册多个事件,那么,可以通过 then 来注册,这种方式可以处理主题的成功、失败和处理中三种事件。

    $.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 );
      });

     如果需要访问两个 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 这个对象的内部,必须有三个回调队列了,这里的成功和失败只能一次完成,所以这两个 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 就是直接注册了两个事件。then 允许我们一次处理三种注册。

    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();
        }
    });
  • 相关阅读:
    第07组 Beta冲刺(2/5)
    第07组 Beta冲刺(1/5)
    第07组 Alpha事后诸葛亮
    第07组 Alpha冲刺(6/6)
    【Beta】软件使用说明——致社长
    【Beta】“北航社团帮”发布声明——小程序v2.0与网页端v1.0
    【Beta】“北航社团帮”测试报告——小程序v2.0与网页端v1.0
    [技术博客] 小程序扫码登录网页端原理
    [技术博客] 用户验证码验证机制---redis缓存数据库的使用
    [技术博客] 如何避免在代码中多重render
  • 原文地址:https://www.cnblogs.com/haogj/p/4480772.html
Copyright © 2020-2023  润新知