jQuery源码解析(2)—— Callback、Deferred异步编程

闲话

这篇文章,一个月前就该出炉了。跳票的原因,是因为好奇标准的promise/A+规范,于是学习了es6的promise,由于兴趣,又完整的学习了《ECMAScript 6入门》

本文目的在于解析jQuery对的promise实现(即Deferred,是一种非标准的promise实现),顺便剖析、挖掘观察者模式的能力。建议读完后参考下面这篇博文的异步编程部分,了解Promise、Generator、Async。

ECMAScript 6规范总结(长文慎入) http://blog.csdn.net/vbdfforever/article/details/50727462


引子

传统的异步编程使用回调函数的形式,当回调函数中调用回调函数时,层层嵌套,且每个回调内部都需要单独捕捉错误,因为执行上下文在同步执行的过程中早就消失无影,无法追溯了。

/* 回调函数 */
step1(function (error, value1) {
    step2(value1, function(error, value2) {
        try {
            // Do something with value2
        } catch(e) {
            // ...
        }
    });
});

我们需要一种新的方式,能够解除主逻辑与回调函数间的耦合(分离嵌套),并保证执行的异步性。

有两种思路:声明式、命令式。对于声明式的解决这类问题,以同步方式书写异步代码,甚至是错误捕捉,需要语言层面的解决,或者至少自己要写一个简单的编译器。我们并不需要实现一个webapp,只是以工具、库的形式存在的组件,因此只考虑在现有语法框架下,使用命令式的方式。

命令式的方法,配上链式调用,最直接的就是下面这种思路(回调之间都被拆分开)

step1().anApi(step2).anApi(step3).catchError(errorFun)

由于事件等待本身不会阻塞javascipt的运行,因此图中的step2、step3、errorFun需要被储存,等待内部合适的时候触发它们。发现了么,这类似于“发布事件,等待被订阅触发”的过程,即观察者模式(也称发布-订阅模式)。

下面用一个(简单到没啥用的)玩具代码来演示如何实现的:

// 观察者(堆栈,提供添加、触发接口)
function watch() {
    var cache = [];
    return {
        done: function(callback) {
            cache.push(callback);
        },
        resolve: function() {
            for (var i=0; i<cache.length; i++) {
                cache[i].apply(this, arguments);
            }
        }
    }
}

function somethingAsync() {
    // some code...
    var lis = watch();
    事件 = function() {
        lis.resolve();
    }
    return lis;  // 返回可以绑定订阅者的接口
}
somethingAsync().done(fn1).done(fn2);


Callback

观察者模式,可以解耦回调函数的绑定。但在这里需要定制两个功能:

1、递延。对于事件,触发的时候如果没有监听,就错过了。保存触发时的参数,添加回调时判断该参数是否已有保存值,决定是否即时调用。

2、once。回调只能被触发一次。

这里需要介绍一个概念:钩子。通过在程序不同的地方埋置钩子,可以增加不同的特性和功能支持。同样是观察者模式,根据不同的需求,需要定制不同的功能。不仅是Deferred,很多时候我们都会用到观察者模型,但是需求的功能特征不同。jQuery抽象出Callback的目的就是尽可能挖掘观察者模式的潜力,实现一个match多个case的强大的观察者模式,并且考虑了循环调用的情况,不仅可以用于Deferred,还可以复用于大部分需要借用观察者模型的其他场合,一劳永逸。比如,实现迭代器的时候,有的return false表示终止,有的却不影响,要想两种都支持,需要增加一个形参,而这里的思路是通过传入字符串参数,指定代码中钩子的状态。

在Callback中,支持memory递延(add时设置)、once单次触发后lock锁定状态(fire时设置)、unique回调去重(add时设置)、stopOnfalse(fire内遍历时判断)。采用核心+外观的形式,内部有一个基本的fire(还有一个基本的add,因为没有别的接口调用直接嵌在外部调用的add内部了),和fire、fireWith外观。增加了锁定、禁用功能。思路是通过locked=true锁定封住外部调用的fire相关接口(除了存在递延memory参数,add接口仍然可以调用内部的fire操作),通过list=”“锁定add操作。因此locked(锁定),locked+list(禁用)。

Callback在1.12版本比1.11版本真心优雅不少,语义更清晰。list代表回调列表,当调用fire遍历list回调列表时,回调函数本身可能又内部调用add或fire,需要考虑。当add时,没什么影响,只需要动态判断list.length就好,fire时,需要先把任务存在任务列表里,queue就相当于任务列表,里面存着每次fire需要使用的参数(参数都是数组形式,所以肯定不是undefined)。使用firing看标记是否属于正在fire阶段。fire的过程中会持续queue.shift()然后遍历回调。外观fire接口,可以拦截locked的情况,不会向queue中push参数。由于递延的效果,add中会涉及直接执行,为了减小复杂度,执行只通过内部fire接口,用firingIndex指定开始执行的索引位置。

[源码]

// #410,Array.prototype.indexOf 兼容,下面会用到
jQuery.inArray = function( elem, arr, i ) {
    var len;

    if ( arr ) {
        if ( var indexOf = [].indexOf ) {
            return indexOf.call( arr, elem, i );
        }

        len = arr.length;
        // x?(x?x:x):x
        i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

        for ( ; i < len; i++ ) {

            // Skip accessing in sparse arrays
            if ( i in arr && arr[ i ] === elem ) {
                return i;
            }
        }
    }

    return -1;
}

// #3159,能把字符串'once memory' -> {'once': true, 'memory': true}
function createOptions( options ) {
    var object = {};
    jQuery.each( options.match( /\S+/g ) || [], function( _, flag ) {
        object[ flag ] = true;
    } );
    return object;
}

// #3189,参数为空格隔开的字符串,定制需要的观察者模型
// 
// options -> 4种模式(钩子),可混合
// once:  保证回调列表只被触发一次
// memory:  能够记忆最近一次触发使用的参数,回调执行时都会使用该参数
// unique:  回调不会被重复添加
// stopOnFalse:  回调返回false中断调用
jQuery.Callbacks = function( options ) {

    // 提取模式
    options = typeof options === "string" ?
        createOptions( options ) :
        jQuery.extend( {}, options );

    var // 是否正在fire触发阶段,用来判断是外部的触发,还是回调函数内部的嵌套触发
        firing,

        // 记录上次触发时使用的参数
        memory,

        // 记录是否已经被触发过至少一次
        fired,

        // 锁定外部fire相关接口
        locked,

        // 回调列表
        list = [],

        // 多次fire调用(因为可能被嵌套调用)的调用参数列表
        queue = [],

        // 回调列表list的触发索引,也会用在指定add递延触发位置
        firingIndex = -1,

        // 内部核心fire接口
        fire = function() {

            // 若只能被触发一次,此时锁定外部fire接口
            locked = options.once;

            // 标记为已触发、且正在触发
            fired = firing = true;
            for ( ; queue.length; firingIndex = -1 ) {
                // fire参数列表取出第一项,开始遍历
                memory = queue.shift();
                // 遍历
                while ( ++firingIndex < list.length ) {

                    // 若执行后返回false,判断是否有stopOnFalse钩子,指定钩子逻辑
                    if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && options.stopOnFalse ) {

                        // queue中本参数对list的遍历到此为止,跳出
                        firingIndex = list.length;
                        // 本参数不会再有递延效果,因为有回调已经返回了false
                        memory = false;
                    }
                }
            }

            // 若无递延效果,queue中最后一个触发参数不会保留
            if ( !options.memory ) {
                memory = false;
            }
            // 结束firing阶段
            firing = false;

            // 如果锁定了(比如once),外部fire封掉了,由是否有递延指定add(会调用内部fire)是否可用,无递延就要disable掉(locked+list)
            if ( locked ) {

                // 'once memory'
                if ( memory ) {
                    list = [];

                // disable()
                } else {
                    list = "";
                }
            }
        },

        // return self
        self = {

            // 添加回调,可以是回调数组集合。支持递延触发内部fire
            add: function() {
                if ( list ) {

                    // 外部显示调用add,判断是否是递延触发时机,memory推入fire列表,重置执行索引位置(递延状态下执行过fire,才不会重置memory)
                    if ( memory && !firing ) {
                        firingIndex = list.length - 1;
                        queue.push( memory );
                    }

                    // 通过递归add,支持[fn1,[fn2,[fn3,fn4]]] -> fn1,fn2,fn3,fn4
                    ( function add( args ) {
                        jQuery.each( args, function( _, arg ) {
                            if ( jQuery.isFunction( arg ) ) {
                                if ( !options.unique || !self.has( arg ) ) {
                                    list.push( arg );
                                }
                            } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {

                                // Inspect recursively
                                add( arg );
                            }
                        } );
                    } )( arguments );

                    // 递延触发
                    if ( memory && !firing ) {
                        fire();
                    }
                }
                // 链式
                return this;
            },

            // 移除回调,支持多参数。去掉所有相同回调,当回调内调用remove时,若删除项为已执行项,要修正firingIndex位置
            remove: function() {
                jQuery.each( arguments, function( _, arg ) {
                    var index;
                    // Array.prototype.indexOf 兼容方法,从index索引位匹配
                    while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
                        list.splice( index, 1 );

                        // 修正firingIndex
                        if ( index <= firingIndex ) {
                            firingIndex--;
                        }
                    }
                } );
                return this;
            },

            // 判断是否有指定回调,无参数则判断回调列表是否空
            has: function( fn ) {
                return fn ?
                    // Array.prototype.indexOf 兼容方法
                    jQuery.inArray( fn, list ) > -1 :
                    list.length > 0;
            },

            // 清空list
            empty: function() {
                // 仅在list不为""时
                if ( list ) {
                    list = [];
                }
                return this;
            },

            // 禁用。list封add,locked封外部fire接口
            disable: function() {
                locked = queue = [];
                list = memory = "";
                return this;
            },
            disabled: function() {
                return !list;
            },

            // 锁定,locked封外部fire接口,是否递延判断add是否可调用内部fire
            lock: function() {
                locked = true;
                // 无递延(每次执行完memory重置为false)或没触发过,则直接禁用
                if ( !memory ) {
                    self.disable();
                }
                return this;
            },
            locked: function() {
                return !!locked;
            },

            // 把调用参数(memory[0]为环境,memory[1]为参数数组)推入queue,制定环境调用fire
            fireWith: function( context, args ) {
                if ( !locked ) {
                    args = args || [];
                    args = [ context, args.slice ? args.slice() : args ];
                    queue.push( args );
                    if ( !firing ) {
                        fire();
                    }
                }
                return this;
            },

            // 调用者为this
            fire: function() {
                self.fireWith( this, arguments );
                return this;
            },

            // 是否触发过
            fired: function() {
                return !!fired;
            }
        };

    return self;
};


Deferred

Deferred是jQuery内部的promise实现,内部使用的是递延(参数记忆)+oncelock(状态锁定)的观察者模型。有三种状态:正常时候是”notify”(没有oncelock),成功后是”resolve”,失败后是”reject”,每种状态使用一个观察者对象。当触发成功或失败时,相反的状态被禁用,但notify状态如果被触发过则不会禁用仅仅lock锁住(仅可以add递延调用,不可以外部触发)。

jQuery的实现的特点是:随意、灵活。这也算是缺点。跟promise/A+标准反差挺大的呢。

jQuery中没有自动的错误捕捉,全靠自觉,reject状态的设置本身也不像是为了错误设置的,如果你代码写太渣,没在合适的地方捕捉并reject,错误确实捉不住。标准中的reject定位就是抛出错误,我猜这应该是大量的实践证明了除了成功主要是用于错误处理吧。而且如果真的需要处理错误,done也不能做到触发下一个promise,只有then的实现可以加工一下做到。

done/fail是直接在Callback的list列表中添加回调,同步执行,回调间不会异步等待。每个then(fun)都返回一个promise,在Callback的list列表中添加一个既执行fun、又触发then内deferred对象的回调函数,若fun返回promise对象,则在其后.done/fail( newDefer.resolve/reject ),实现异步串起回调。

Deferred也是使用了两种编程方式的雏形,一种是把deferred当做一个对象,需要的时候deferred,另一种是用它包裹函数Deferred(fun),函数内封装业务逻辑,优点是可以通过依赖注入的方式实现功能,可以减少暴露外部的接口,如果平常用的少可能一时不大得心应手。当然,由于Deferred两种编程方式都使用了,减少暴露接口的特点就没有利用了。在标准的实现中,只用了第二种方式,真正意义的隐藏了resolve/reject接口(即不是返回完整的deferred)。

[源码]

// #3384,Deferred,使用闭包式写法(非面向对象式,由于add/done接口暴露,所以是可以实现面向对象式的,原型上的then可以调用到add/done)
jQuery.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",

        // 不含resolve/reject接口的promise
        promise = {
            state: function() {
                return state;
            },
            always: function() {
                deferred.done( arguments ).fail( arguments );
                return this;
            },

            // 注意:每个then返回一个全新deferred对象的promise
            then: function( /* fnDone, fnFail, fnProgress */ ) {
                var fns = arguments;

                // 依赖传入,新生成的deferred,返回deferred.promise()
                return jQuery.Deferred( function( newDefer ) {
                    jQuery.each( tuples, function( i, tuple ) {
                        // tuples中对应tuple的对应回调函数
                        var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];

                        // tuples中对应tuple的对应[ 'done' | 'fail' | 'progress' ]
                        // promise[ 'done' | 'fail' | 'progress' ]在下面被遍历添加
                        deferred[ tuple[ 1 ] ]( function() {
                            var returned = fn && fn.apply( this, arguments );

                            // 返回promise或deferred对象时,异步触发newDefer对应状态
                            if ( returned && jQuery.isFunction( returned.promise ) ) {
                                returned.promise()
                                    .progress( newDefer.notify )
                                    .done( newDefer.resolve )
                                    .fail( newDefer.reject );
                            } else {

                                // 非promise对象,跟done/fail效果相当,但却是通过触发下一个promise的形式。若返回值存在,参数为返回值,否则为done/fail遍历调用的argument
                                newDefer[ tuple[ 0 ] + "With" ](
                                    this === promise ? newDefer.promise() : this,
                                    fn ? [ returned ] : arguments
                                );
                            }
                        } );
                    } );
                    fns = null;
                } ).promise();
            },

            // 无参数时,返回不含resolve/reject接口的promise对象,可循环调用
            // 有参数可扩展,生成如deferred对象
            promise: function( obj ) {
                return obj != null ? jQuery.extend( obj, promise ) : promise;
            }
        },
        deferred = {};

    // 别名,不清楚是用来兼容在什么情况[摊手]
    promise.pipe = promise.then;

    // 为promise接口添加与Callback对象交互的done(对应add)/fail/progress方法
    // 为deferred对象添加与Callback对象交互的resolve/resolveWith(对应fireWith)/reject/rejectWith
    jQuery.each( tuples, function( i, tuple ) {
        // 对应观察者模型Callback
        var list = tuple[ 2 ],
            // 对应状态
            stateString = tuple[ 3 ];

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

        // 'resolved' 'rejected'
        if ( stateString ) {
            list.add( function() {

                // state = [ resolved | rejected ]
                state = stateString;

            // [ reject_list | resolve_list ].disable(相反观察者禁用); progress_list.lock(progress锁定)
            // ^ 按位异或,0^1 = 1,1^1 = 0,(二进制写法取不同位为1,相同位为0)
            }, 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;
    } );

    // 合并成最终的deferred,promise相当于deferred的一个子集。deferred.promise() -> promise
    promise.promise( deferred );

    // 执行fun,并传入生成的deferred(对第二种编程形式的支持)
    if ( func ) {
        func.call( deferred, deferred );
    }

    // 返回deferred
    return deferred;
};


when

when方法返回一个deferred的promise对象。接受多个参数,没有promise接口的参数当做resolved状态,当参数中全部变为resolved状态时,会触发when中deferred的resolve。当有一个参数变成reject,会触发deferred的reject。当有参数调用notify时,每次调用都会执行一次。除了reject是使用触发项的触发参数外,resolve和reject均使用一个参数数组触发,数组中每一项对应when中参数每一项的触发参数,对于when参数中的非promise对象,对应的触发参数就是它们自身。

when还考虑到只有一个参数,且带有promise方法时,可以直接使用该参数来触发成功操作,节省开销,因此方法开头做了这个优化。因此这种情况,直接由该对象接管。触发的参数规则的不一致,个人认为很不优雅,而且updateFun里arguments.length<=1时,也不一致。

// #3480
jQuey.when = function( subordinate /* , ..., subordinateN */ ) {
    var i = 0,
        resolveValues = slice.call( arguments ),
        length = resolveValues.length,

        // 判断是否单参数且带有promise方法
        remaining = length !== 1 ||
            ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,

        // 新生成Deferred对象,对单参数且带有promise方法进行优化
        deferred = remaining === 1 ? subordinate : jQuery.Deferred(),

        updateFunc = function( i, contexts, values ) {
            // progress触发器、resolve触发器(根据计数器判断是否触发)
            return function( value ) {
                // 设置当前触发项的环境
                contexts[ i ] = this;
                // 设置resolve/progress对应的触发参数的数组中的该位置的参数
                values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;

                // 若触发的是progress操作
                if ( values === progressValues ) {
                    deferred.notifyWith( contexts, values );

                // 触发的是resolve。计数器减至0才会触发新defer的resolve,使用resolve对应的触发参数的数组
                } else if ( !( --remaining ) ) {
                    deferred.resolveWith( contexts, values );
                }
            };
        },

        progressValues, progressContexts, resolveContexts;

    // length为0会在if ( !remaining ){}直接调用resolve,为1时由于是参数本身,
    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()
                    .progress( updateFunc( i, progressContexts, progressValues ) )
                    .done( updateFunc( i, resolveContexts, resolveValues ) )
                    .fail( deferred.reject );
            } else {
                // 遇到不带promise接口的参数计数变量-1
                --remaining;
            }
        }
    }

    // 若同步执行到此处时,已经是全resolved状态,则直接触发resolve
    if ( !remaining ) {
        deferred.resolveWith( resolveContexts, resolveValues );
    }

    return deferred.promise();
};

结尾:建议再参考es6规范总结的异步编程一节。文章开头给出了地址。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值