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规范总结的异步编程一节。文章开头给出了地址。
- jQuery源码解析(2)—— Callback、Deferred异步编程
- jquery Deferred异步回调源码解析应用
- jQuery源码解析-----Deferred是什么?
- jQuery源码分析-05异步队列 Deferred
- jquery 异步Deferred
- JS异步编程(promise、deferred对象)
- Deferred 实现异步编程
- jQuery源码学习(版本1.11)-Deferred
- jQuery的deferred对象实现callback
- jQuery异步框架探究2:jQuery.Deferred方法
- dojo异步编程 dojo/Deferred
- jquery 源码分析之Deferred
- jQuery callback源码分析
- 利用 Jquery Deferred 异步你的程序
- jquery Deferred 解决异步回调问题
- JQuery之Callback源码分析
- jQuery 2.0.3 源码分析 Deferred概念
- jQuery的deferred对象详解--源码
- 线程锁的概念函数EnterCriticalSection和LeaveCriticalSection的用法
- stl: map和hashmap比较 hashmap与hashtable
- struts 下载地址
- 我的Exchange 2010 启用匿名了。我怎么防止别人任意伪造一个邮件地址发送给我内部的人呢?
- 人生的一种境界:享受第四种感情
- jQuery源码解析(2)—— Callback、Deferred异步编程
- Maven入门
- kafka源码解析之三Broker的启动
- 梳理caffe代码math_functions(一)
- 加油宝贝!
- struts2的整理
- hdu 3413
- 而立之年!!!
- 第4周项目3 小学生算数能力测试系统