前言
接上一篇《jQuery源码剖析(二)——$.Callbacks》,对于javascript来说,回调这个概念无处不在,bang上次还抱怨nodejs里边读取文件内容都要回调,这里读取文件内容其实就是异步的场景。异步真是无处不在的:ajax请求,页面动画。
对于读取文件内容这个场景,通过$.Callbacks来表述读取某个文件的场景,就是以下代码:
1 2 3 4 5 6 7 8 9 10 | var callbacks = $.Callbacks(); callbacks.add(function(){ alert('读取到内容'); }); callbacks.add(function(content){ alert('内容:' + content); }); read('raphealguo.txt', function(content){ callbacks.fire(content); }); |
显然这样的做法不优雅,每次要读一个文件必须定义一个callback管理器实例。
而且读取文件有可能有失败的情况,这样就还得要去定义多一个callback管理器去处理失败的回调队列。
既然代码如此之不优雅,当然要寻求一个解决方案,例如:
1 2 3 4 | read("raphealguo.html") .done(function(){ alert("读取到内容"); }) .done(function(content){ alert('内容:' + content); }) .fail(function(){ alert("读取文件失败"); }); |
链式结构,表达清晰!
这个就是jQuery里边的异步队列Deferred(其实这里的deferred是延迟处理,但是姑且叫做异步队列吧)的做法。
什么是Deferred
从上边例子可以看到readfile(“raphealguo.html”)返回一个我们成为deferred的实例,在这上边可以绑定一些成功失败的回调。
在实际的开发中,其实会经常遇到一些很耗时的操作,例如ajax请求数据,read读取文件,处理大数据等等。由于不能立即得到结果,所以我们需要一个延迟的回调。
$.Deferred就是这样诞生的,jQuery里边的ready,ajax都是用了异步队列deferred。
看一下read的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | function read(path){ var dfd = $.Deferred();//jQuery的异步队列 console.log("开始读取"); setTimeout(function(){ console.log("结束读取"); dfd.resolve(path + '的内容');//resolve类似$.Callbacks的fire,派发事件,由done接管 //dfd.reject('失败');//由fail接管 }, 3000); dfd.notify('处理中');//fire派发事件,由progress方法接管 return dfd;//返回异步队列,dfd包含了done,progress,fail方法 } read("test.html") .done(function(content){ console.log(content);}) .fail(function(){ console.log("出错!"); } ) .progress(function(){ console.log("处理中!"); }); //输出: //开始读取 //处理中! //结束读取 //test.html的内容 |
可以看到其实这里的延迟队列有三种状态:成功|失败|处理中。
在上边的例子里边只用到了两个状态:成功(由resolve派发通知done),处理中(由notify派发通知progress)
因此,$.Deferred实例里边维护了三个$.Callbacks管理器,分别管理以上三个状态的回调队列。
Deferred.promise
再看看刚刚的例子,如果是以下代码的话:
1 2 3 4 5 | var dfd = read("test.html") .done(function(content){ console.log(content);}) .fail(function(){ console.log("出错!"); } ) .progress(function(){ console.log("处理中!"); }); dfd.resolve('非法的触发');//这里竟然也能触发done回调 |
对于read接口来说,把触发的接口(resolve|notify|reject)暴露出去是一件很危险的事情。
也就是说,对于read来说,我们暴露出去的dfd最好只有done|fail|progress三个接口。
于是promise就出现了!
1 2 3 4 5 | function read(path){ var dfd = $.Deferred();//jQuery的异步队列 /* other code */ return dfd.promise();//通过promise方法把dfd里边一些状态设置的接口给隐藏掉,只暴露done|fail|progress三个接口。 } |
如果说在read函数里边不想要每次自己创建一个dfd,可以直接使用$.Deferred接口。
如下:
1 2 3 4 5 6 7 8 9 | function read(/*path*/){//这里暂时忽略掉path参数 var dfd = this;//在Deferred里边已经把上下文切换成它生成的一个异步队列实例 /* other code */ return dfd.promise();//通过promise方法把dfd里边一些状态设置的接口给隐藏掉,只暴露done|fail|progress三个接口。 } $.Deferred(read) .done(function(content){ console.log(content);}) .fail(function(){ console.log("出错!"); } ) .progress(function(){ console.log("处理中!"); }); |
Deferred成员方法
-
$.Deferred()
生成一个异步队列实例
接受一个function参数,function里边可以使用this来调用当前的异步队列实例
-
deferred.done(fn)
成功时触发的回调fn
-
deferred.fail(fn)
失败时触发的回调fn
-
deferred.progress(fn)
处理中触发的回调fn
-
deferred.resolve/resolveWith([context], args)
这里等同$.Callbacks().(fire/fireWith)
在任务处理成功之后使用此方法触发成功事件,之前加入done队列的回调会被触发
-
deferred.reject/rejectWith([context], args)
这里等同$.Callbacks().(fire/fireWith)
在任务处理失败之后使用此方法触发失败事件,之前加入fail队列的回调会被触发
-
deferred.notify/notifyWith([context], args)
这里等同$.Callbacks().(fire/fireWith)
在任务处理中可以使用此方法触发正在处理事件,之前加入progress队列的回调会被触发
-
deferred.promise()
简单理解就是生成一个跟deferred一样的对象,但是无法在外部用resolve等去修改当前任务状态
-
deferred.then(/* fnDone, fnFail, fnProgress */)
可以直接传入三个回调函数,分别对应done|fail|progress三个状态的回调
-
deferred.always(fn)
不管最后是resolve还是reject,都会触发fn
-
$.when(mission1, [mission2, mission3, ...])
这个是绑定在jQuery上的
可以接受多个任务。如:$.when(readfile1, readfile2).done(/* Your code */)
$.Deferred源码
当中有一些羞涩难以理解的地方,希望能够看到这篇博客的人一起讨论一下,详细看代码注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | $.Deferred = function( func ) { var tuples = [ // action, add listener, listener list, final state //三个队列,done|fail|progress 成功|失败|处理中 //分别对应: //事件 暴露的添加回调的接口名 Callbacks队列 处理结束后的状态 [ "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; }, //deferred.done(fnDone),fail(fnFail).progress(fnProgress)的快捷方式 then: function( /* fnDone, fnFail, fnProgress */ ) { // fns = [fnDone, fnFail, fnProgress] var fns = arguments; //这里return jQuery.Deferred(function( newDefer ) {}).promise(); //为何还要使用jQuery.Deferred来包装 //就then比较特殊需要重新调promise方法来屏蔽resolve|reject|notify这些接口? return jQuery.Deferred(function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { //action = [resolve | reject | notify] var action = tuple[ 0 ], //分别对应fnDone, fnFail, fnProgress fn = fns[ i ]; //为何这里不能直接:deferred[ tuple[1] ](fn) // deferred[ done | fail | progress ] for forwarding actions to newDefer // tuple[1] = [ done | fail | progress ] deferred[ tuple[1] ]( jQuery.isFunction( fn ) ? function() { //当前的this == deferred var returned = fn.apply( this, arguments ); //如果回调返回的是一个Deferred实例 if ( returned && jQuery.isFunction( returned.promise ) ) { //则继续派发事件 returned.promise() .done( newDefer.resolve ) .fail( newDefer.reject ) .progress( newDefer.notify ); } //如果回调返回的是不是一个Deferred实例,则被当做args由XXXWith派发出去 else { newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); } } : //传进来的不是函数 //则默认调用[resolve | reject | notify]派发事件出去 newDefer[ action ] ); }); //这里的fns已经没用了,有用的fn引用已经被记录了 //退出前手工设置null避免闭包造成的内存占用 fns = null; }).promise(); }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object //在这里obj绑定了[resolve | reject | notify]这些方法 //还记得例子中的 return deferred.promise() //就是因为不想把[resolve | reject | notify]这几个函数暴露出去 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 //初始化三条Callbacks队列 jQuery.each( tuples, function( i, tuple ) { var list = tuple[ 2 ],//队列 stateString = tuple[ 3 ];//最后状态 // promise[ done | fail | progress ] = list.add //tuple[1] == done | fail | progress //可以看到 done|fail|progress其实就是Callbacks里边的add方法 promise[ tuple[1] ] = list.add; // Handle state //成功|失败 //处理中是没有最后状态的! if ( stateString ) { //成功和失败默认的3个回调 list.add( //1、修改最终状态 function() { // state = [ resolved | rejected ] state = stateString; }, //2、禁用对立的那条队列 //注意 0^1 = 1 1^1 = 0 //即是成功的时候,把失败那条队列禁用 //即是成功的时候,把成功那条队列禁用 // [ reject_list | resolve_list ].disable; progress_list.lock tuples[ i ^ 1 ][ 2 ].disable, //3、锁住当前队列状态 tuples[ 2 ][ 2 ].lock ); } // deferred[ resolve | reject | notify ] = list.fire //tuple[0] == resolve | reject | notify //可以看到 resolve|reject|notify其实就是Callbacks里边的fire方法 //而resolveWith|rejectWith|notifyWith其实就是Callbacks里边的fireWith方法 deferred[ tuple[0] ] = list.fire; deferred[ tuple[0] + "With" ] = list.fireWith; }); // Make the deferred a promise //这一步之前promise和deferred绑定了以下方法 // deferred[ resolve | reject | notify ] // deferred[ resolveWith | rejectWith | notifyWith ] // promise[ done | fail | progress | then | always | state | promise ] //调用内部辅助的promise的promise方法(jQ坑爹,起同样名字) //扩展deferred的then | done | fail | progress等方法 promise.promise( deferred ); //这里为什么要分成promise跟deferred两个对象呢 //其实就是因为不想把[resolve | reject | notify]这几个函数暴露出去 //见上边的promise()源码 // Call given func if any //$.Deferred(func)格式,则自动执行任务func //并且把当前任务的上下文跟参数设置成当前生成的deferred实例 if ( func ) { func.call( deferred, deferred ); } // All done! //返回实例 return deferred; } |
$.when源码
不知道大家看完$.when之后,是否可以简单利用$.when写一个简单的js的模块加载器(只有一层依赖的情况)。
其实Seajs、RequireJs这些都算是一个多个异步任务处理的问题,可以用这个思路去设计这样一个框架(当然还要稍微复杂一点点)。
以下是$.when的源码注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | // Deferred helper //注意到$.when是多任务的 //当一个任务失败的时候,代表整个都失败了。 //任务是Deferred实例,我成为异步任务 //任务是普通function时,我成为同步任务 $.when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, //arguments是多个任务 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 ) ) {//成功,并且最后剩余的异步任务为0了 //说明所有任务都成功了,派发成功事件出去 //事件包含的上下文是当前任务前边的所有任务的一个集合 deferred.resolveWith( contexts, values ); } }; }, progressValues, progressContexts, resolveContexts; // add listeners to Deferred subordinates; treat others as resolved //如果只有一个任务,可以不用做维护状态的处理了 //只有大于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() //成功的时候不断更新自己的状态 .done( updateFunc( i, resolveContexts, resolveValues ) ) //当一个任务失败的时候,代表整个都失败了。直接派发一个失败即可 .fail( deferred.reject ) //正在处理的时候也要不断更新自己的状态 .progress( updateFunc( i, progressContexts, progressValues ) ); } else { //如果是同步任务,则remain不应该计它在内 --remaining; } } } // if we're not waiting on anything, resolve the master //传进来的任务都是同步任务 if ( !remaining ) { deferred.resolveWith( resolveContexts, resolveValues ); } return deferred.promise(); } |
下一篇就是真正要开始$(document).ready(fn)了。
本文链接:jQuery源码剖析(三)——$.Deferred
转载声明:本博客文章若无特别说明,皆为原创,转载请注明来源:拉风的博客,谢谢!
看看jquery1.7的代码:https://github.com/jquery/jquery/blob/1.7rc2/src/deferred.js
1.7时defer源码里的then是简单的快捷方法,没其他功能。
1.8以前then的效果:
var defer = $.Deferred(),
filtered = defer.then(function( value ) {
//defer的一个done回调
}).done(function( value ) {
//defer的另一个done回调,跟then无关系,
});
defer.resolve( 5 );
到了jquery1.8以后,then和pipe方法合并了,不知为啥要合并,可能觉得多了个一样的接口迷惑吧。
用pipe这个名字就比较好理解,把回调方法放入一个管道里,这个回调方法完成后还可以通过管道进入另一个回调。有一个用法是使用then做数据过滤:
var defer = $.Deferred(),
filtered = defer.then(function( value ) {
return value * 2;
}).done(function( value ) {
//这里的done是then里的函数执行完后的defer发起的,而不是原defer发起
//then里处理过后的数据就可以“流”到这里了,所以还是用名字pipe比较好理解
alert(“Value is ( 2*5 = ) 10: ” + value );
});;
defer.resolve(5);
恩,用pipe的概念确实好理解,代码里边为什么不直接
:deferred[ tuple[1] ](fn)
就是因为fn处理后的值需要流道下一个done的回调(或其他回调)里边去
模拟了Linux地下管道 “|” 的概念:
grep a | grep b | .. | ..