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

来源:互联网 发布:网络情感诈骗的方法 编辑:程序博客网 时间:2024/06/05 00:38

闲话

这篇文章,一个月前就该出炉了。跳票的原因,是因为好奇标准的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时,也不一致。

// #3480jQuey.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 0