破解jQuery Deferred()异步执行的神秘世界
本文解析jQuery.Deferred()源代码,学习异步执行实现原理和编程技术。
测试代码:
var f1=function(){
var deferred = $.Deferred();
deferred.resolve('f1 resolved');
return deferred.promise();
}
f1().then(function(data){
//$.when().then(function(){
console.log(data);
var deferred = $.Deferred();
setTimeout(function(){
deferred.resolve('then resolved');
},2000);
return deferred.promise();
}).done(function(data){ // 也可以写.then(f1,f2,f3)
console.log(data);
}).fail(function(){
console.log('fail');
}).progress(function(){
console.log('progress');
});
jQuery Deferred()源代码分为两个嵌入到jquery对象的函数块:
jQuery.Callbacks = function( options ) {
定义promise/deferred对象的内部操作方法以及内部数据...
jQuery.extend({
Deferred: function( func ) {
定义promise/deferred对象以及暴露的方法比如resolve/then...
Deferred()代码:
Deferred: function( func ) {
var tuples = [
// action, add listener, listener list, final state
[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], // jQuery.Callbacks调用结果是返回一个self{}对象,含内部处理函数
[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
[ "notify", "progress", jQuery.Callbacks("memory") ]
],
state = "pending", // 这个就是当前Deferred实例的状态
promise = {}; //返回的实例(子实例)
deferred = {}; //当前实例(父实例,含子实例)
jQuery.each( tuples, function( i, tuple ) {
var list = tuple[ 2 ], // tuples是调Callbacks获取self对象,因此list引用self对象
// deferred[ resolve | reject | notify ]
deferred[ tuple[0] ] = function() { // 循环三次分别构造deferred.resolve/reject/notify函数代码
deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); //调deferred.resolveWith,构造参数,resolve函数是deferrred
的内置函数,this就是deferred,arguments就是调resolve时的入口参数,比如resolve(data)。此句即为:
deferred.resolveWith(promise,arguments);
传递的作用域object是promise而不是deferred。
resolveWith就是fireWith,因此就是调fireWith(promise.arguments),fireWith函数代码如下:
// Call all callbacks with the given context and arguments
fireWith: function( context, args ) { // context是promise,args是调resolve传递的参数
fire( args ); // fire函数在jQuery.Callbacks函数中定义,下面是Callbacks函数结构
//jQuery.Callbacks相当于定义一个类,从外部调self里面的public方法,public方法再调private方法
jQuery.Callbacks = function( options ) {
list = [], // 执行then时把内部callback存储在list[],内部callback会调用应用callback
// Fire callbacks
fire = function( data ) {
list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) // 执行list[]里面的内部callback
}
// Actual Callbacks object
self = {
// Call all callbacks with the given context and arguments
fireWith: function( context, args ) {
fire( args );
}
}
return self; // Deferred()会调用这个函数,引用self,因此Callbacks函数执行完之后self还存在,list[]也是。
deferred[ tuple[0] + "With" ] = list.fireWith; // deferred.resolveWith
//因此resolve方法其实就是调用 resolve -> resoveWith = list.fireWith (引用self) -> fire (执行list[]里面的callback),代码设计非常复杂深奥,它没有用new实例方法,而是利用了js对象引用机制,函数定义一个对象,在其它位置引用这个对象,这个对象就一直存在,直到引用它的函数不存在为止,其实用new实例方法可以简化设计不用这么深奥。
});
下面来看then代码,Deferrred最复杂深奥的代码在then代码,体现了作者高超的编程技术,令人叹为观止:
then: function( /* fnDone, fnFail, fnProgress */ ) {
var fns = arguments;
return jQuery.Deferred(function( newDefer ) { // then是Deferred实例里面的方法,注意这是递归再次调用Deferred创建子实例,当前有一个deferred,又递归再次执行函数新建一个deferred,至此就有两个deferred实例,也就是父子实例,子实例返回以便连写.then(callback),对于下一个.then来说就是父实例,执行下一个.then时又递归执行Deferred函数新建一个子实例返回,以此类推,连写嵌套多少层都一样。
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() { // 循环三次就是执行:
//deferrred.done(callback); // done是注册callback,意思是完成以后执行callback,前一个promise对象resolve时会执行callback。
//deferrred.reject(callback); // 类似
//deferrred.notify(callback); // 类似
//每次都用相同的callback。
//这是注册内部callback(匿名函数),要注册到当前deferred实例,deferred是then所在的实例,then递归调用Deferred新产生的实例也是deferred,但它是传递给回调,因此回调的newDefer是新建的子实例。奥妙就在于回调匿名函数function(newDefer){},在递归调用Deferred新建一套实例时用回调函数代码引用当前已经存在的实例!
//当resolve时会执行内部callback匿名函数,会执行应用callback(也就是then里面的callback),然后再resolve当前promise对象,执行下一个then里面的callback。
//为什么不直接执行应用callback是因为要处理解决当前promise对象,要判断应用callback是否返回promise对象,所以写了一段内部callback。
var returned = fn && fn.apply( this, arguments ); //fn是从then参数获取的应用callback,执行then里面的应用callback。
if ( returned && jQuery.isFunction( returned.promise ) ) { //下面代码的目的就是要 resolve当前then返回的promise对象从而执行下一个then里面的callback
returned.promise()
.done( newDefer.resolve ) //如果执行then里面的callback返回promise对象,就注册一个callback,否则就直接执行resolveWith
.fail( newDefer.reject )
.progress( newDefer.notify );
} else {
newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
// 第一次循环时执行newDefer.resolveWith(promise,[returned]),就是直接去执行下一个then里面的callback并且传递执行应用callback返回的数据。
//其它循环执行rejectWith/NotifyWith,类似。
}
});
fns = null;
}).promise(); //用promise()方法获取promise对象实例返回,promise对象实例只有部分方法,而deferred对象实例有所有的方法
},
可能有点乱有点晕,下面小结一下:
执行then时把内部callback注册存储在list[],当resolve当前promise对象时,就是去list[]找callback执行,内部callback会执行应用callback(then里面的callback),再根据返回值resolve当前promise对象,从而执行下一个then里面的callback。
jQuery Derferred代码比es6-promise复杂,es6-promise用父子实例,then返回的实例是子实例,then把callback存储到父实例,这样当resolve父实例时就从父实例找callback执行即可,比较简单。但jQuery defer设计了一个Callbacks函数,相当于定义了一个类,有public/private方法,有self{}和list[]。执行then时构造一个内部callback存储在当前实例(父实例)的list[],当resolve当前实例时就去list[]找内部callback执行,内部callback会执行应用callback(then里面的callback),再resolve返回的子实例去list[]执行内部callback,这就是执行下一个then()里面的callback。每一层then都要重新递归执行jQuery.Deferred(func)新建一套子实例返回,下一个then就是新建子实例的内置方法,当执行下一个then时,then所在的实例就是父实例,递归执行Deferred新建实例就是子实例,如果连写多个.then则如此层层嵌套递归。递归是很复杂的,它是把当前程序代码又执行一遍,又产生一套实例,两套代码和实例完全一样,嵌套递归多少层重复执行多少次都不出错,如何区分处理父子实例需要高超的对象编程技术。
所以jQuery Deferred程序和前端框架编译程序一样都是递归程序,重复执行多次就产生多个实例,就递归多个子节点,每一次递归执行时的位置和环境有所变化。
所以通用软件要解决复杂编程技术问题,而相比之下应用软件一般都不涉及复杂编程技术,是两个相当不同的编程世界。
最后一个问题就是如何修改实例的state状态:
state是在Deferred实例中定义的:
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",
有一个内部callback负责修改state状态,其代码和注册位置:
jQuery.each( tuples, function( i, tuple ) {
stateString = tuple[ 3 ];
// Handle state
if ( stateString ) { // resolved或rejected
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 ); //如果resolved就disable rejected,如果rejected就disable resolved
}
执行内部callback时传递promise为作用域:
list[ firingIndex ].apply( data[ 0 ], data[ 1 ] // data[0]=promise data[1]=resolve参数
那么执行function(){state = stateString;} 时就是修改实例中的state,传递作用域为promise有何意义?执行这个方法时是在Callbacks程序空间,如何能访问到deferred实例中的state呢?
这个函数是在Deferred程序空间构造的,因此可以访问state,很简单,虽然把它放到list[]中,再调用执行,但函数是对象,就是引用,这个函数对象是在Deferred
程序空间定义的,就能访问Deferred程序空间的变量数据。
jQuery Deferred没有采用new实例方法,而是利用js对象引用机制,给阅读程序带来很大的迷惑,函数封装绕来绕去就是访问函数定义的一个对象,按程序机制,函数执行完就清除不存在了,包括局部数据都清除不存在了,但它还能在之后访问函数定义的对象。
promise机制其实类似trigger/on机制,都是先注册保存callback,再事件触发执行callback,只是它可以连写,形式上非常直观简洁,能连写是因为它返回promise对象,返回promise对象可不简单,就要涉及递归编程技术,trigger/on之类的程序不是递归程序,所以不能连写,能连写必然是递归程序。而callback的注册保存方法各种软件各不相同,用new实例方法最简明,没有迷惑,不用new实例方法就更复杂,很迷惑,因为那么多独立定义的函数和变量,在不同的程序空间,有些看似没关系,你如何能保证随时随地从某一个函数能访问到某一个变量?
还有一些处理细节也挺复杂的,本文不再分析。。。
bye