在工作中我们可能会把jquery选择做自己项目的基础库,因为其提供了简便的dom选择器以及封装了很多实用的方法,比如$.ajax(),它使得我们不用操作xhr和xdr对象,直接书写我们的代码逻辑即可。更为丰富的是它在es6没有原生支持的那段时间,提供了deferred对象,类似于promise对象,支持done/fail/progress/always方法和when批处理方法,这可能在项目上帮助过你。
es6提供了promise对象,但由于它是内置c++实现的,所以你也没法看它的设计。不如我们通过jquery的源码来探究其设计思路,并比较一下两者的区别。本文采用jquey-3.1.2.js版本,其中英文注释为原版,中文注释为我添加。
jquery的ajax总体设计jquery在内部设置了全局的ajax参数,在每一个ajax请求初始化时,用传递的参数与默认的全局参数进行混合,并构建一个jqxhr对象(提供比原生xhr更为丰富的方法,同时实现其原生方法),通过传递的参数,来判断其是否跨域、传递的参数类型等,设置好相关头部信息。同时其被初始化为一个内置deferred对象用于异步操作(后面讲到),添加done/fail方法作为回调。同时我们也封装了$.get/$.post方法来快捷调用$.ajax方法。
上面提到的deferred对象,与es6的promise对象类似,用于更为方便的异步操作,多种回调以及更好的书写方式。提供progress/fail/done方法,并分别用该对象的notify/reject/resolve方法触发,可以使用then方法快速设置三个方法,使用always添加都会执行的回调,并且提供when方法支持多个异步操作合并回调。可以追加不同的回调列表,其回调列表是使用内部callbacks对象,更方便的按照队列的方式来进行执行。
callbacks回调队列对象,用于构建易于操作的回调函数集合,在操作完成后进行执行。支持四种初始化的方式once/unique/memory/stoponfalse,分别代表只执行依次、去重、缓存结果、链式调用支持终止。提供fired/locked/disabled状态值,代表是否执行过、上锁、禁用。提供add/remove/empty/fire/lock/disable方法操作回调函数队列。
主要涉及到的概念就是这三个,不再做延伸,三个对象的设计代码行数在1200行左右,断断续续看了我一周 (´ཀ`」 ∠) 。我们从这三个倒序开始入手剖析其设计。
jquery.callbacks对象callbacks对象,用于管理回调函数的多用途列表。它提供了六个主要方法:
add: 向列表中添加回调函数
remove: 移除列表中的回调函数
empty: 清空列表中的回调函数
fire: 依次执行列表中的回调函数
lock: 对列表上锁,禁止一切操作,清除数据,但保留缓存的环境变量(只在memory参数时有用)
disable: 禁用该回调列表,所有数据清空
在初始化时,支持四个参数,用空格分割:
once: 该回调列表只执行依次
memory: 缓存执行环境,在添加新回调时执行先执行一次
unique: 去重,每一个函数均不同(指的是引用地址)
stoponfalse: 在调用中,如果前一个函数返回false,中断列表的后续执行
我们来看下其实例使用:
let cl = $.callbacks('once memory unique stoponfalse');
fn1 = function (data) {
console.log(data);
};
fn2 = function (data) {
console.log('fn2 say:', data);
return false;
};
cl.add(fn1);
cl.fire('nicholas'); // nicholas
// 由于我们使用memory参数,保存了执行环境,在添加新的函数时自动执行一次
cl.add(fn2); // fn2 say: nicholas
// 由于我们使用once参数,所以只能执行(fire)一次,此处无任何输出
cl.fire('lee');
// 后面我们假设这里没有传入once参数,每次fire都可以执行
cl.fire('lee'); // lee fn2 say: lee
// 清空列表
cl.empty();
cl.add(fn2, fn1);
// 由于我们设置了stoponfalse,而fn2返回了false,则后添加的fn1不会执行
cl.fire('nicholas'); // fn2 say: nicholas
// 上锁cl,禁用其操作,清除数据,但是我们添加了memory参数,它依然会对后续添加的执行一次
cl.lock();
// 无响应
cl.fire();
cl.add(fn2); // fn2 say: nicholas
// 禁用cl,禁止一切操作,清除数据
cl.disable();
除了上面所说的主要功能,还提供has/locked/disabled/firewith/fired等辅助函数。
其所有源码实现及注释为:
jquery.callbacks = function( options ) {
options = typeof options === "string" ?
// 将字符串中空格分割的子串,转换为值全为true的对象属性
createoptions( options ) :
jquery.extend( {}, options );
var // flag to know if list is currently firing
firing,
// last fire value for non-forgettable lists
memory,
// flag to know if list was already fired
fired,
// flag to prevent firing
locked,
// actual callback list
list = [],
// queue of execution data for repeatable lists
queue = [],
// index of currently firing callback (modified by add/remove as needed)
firingindex = -1,
// fire callbacks
fire = function() {
// enforce single-firing
locked = locked || options.once;
// execute callbacks for all pending executions,
// respecting firingindex overrides and runtime changes
fired = firing = true;
// 为quene队列中不同的[context, args]执行list回调列表,执行过程中会判断stoponfalse中间中断
for ( ; queue.length; firingindex = -1 ) {
memory = queue.shift();
while ( ++firingindex < list.length ) {
// run callback and check for early termination
if ( list[ firingindex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
options.stoponfalse ) {
// jump to end and forget the data so .add doesn't re-fire
firingindex = list.length;
memory = false;
}
}
}
// forget the data if we're done with it
if ( !options.memory ) {
memory = false;
}
firing = false;
// clean up if we're done firing for good
// 如果不再执行了,就将保存回调的list清空,对内存更好
if ( locked ) {
// keep an empty list if we have data for future add calls
if ( memory ) {
list = [];
// otherwise, this object is spent
} else {
list = "";
}
}
},
// actual callbacks object
self = {
// add a callback or a collection of callbacks to the list
add: function() {
if ( list ) {
// if we have memory from a past run, we should fire after adding
// 如果我们选择缓存执行环境,会在新添加回调时执行一次保存的环境
if ( memory && !firing ) {
firingindex = list.length - 1;
queue.push( memory );
}
( 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 a callback from the list
// 移除所有的相同回调,并同步将firingindex-1
remove: function() {
jquery.each( arguments, function( _, arg ) {
var index;
while ( ( index = jquery.inarray( arg, list, index ) ) > -1 ) {
list.splice( index, 1 );
// handle firing indexes
if ( index <= firingindex ) {
firingindex--;
}
}
} );
return this;
},
// check if a given callback is in the list.
// if no argument is given, return whether or not list has callbacks attached.
// 检查是否存在该函数,如果不传递参数,则返回是否有回调函数
has: function( fn ) {
return fn ?
jquery.inarray( fn, list ) > -1 :
list.length > 0;
},
// remove all callbacks from the list
empty: function() {
if ( list ) {
list = [];
}
return this;
},
// disable .fire and .add
// abort any current/pending executions
// clear all callbacks and values
// 置locked为[],即!![] === true,同时将队列和列表都清空,即禁用了该回调集合
disable: function() {
locked = queue = [];
list = memory = "";
return this;
},
disabled: function() {
return !list;
},
// disable .fire
// also disable .add unless we have memory (since it would have no effect)
// abort any pending executions
// 不允许执行,但如果有缓存,则我们允许添加后在缓存的环境下执行新添加的回调
lock: function() {
locked = queue = [];
if ( !memory && !firing ) {
list = memory = "";
}
return this;
},
locked: function() {
return !!locked;
},
// call all callbacks with the given context and arguments
// 为fire附带了一个上下文来调用fire函数,
firewith: function( context, args ) {
if ( !locked ) {
args = args || [];
args = [ context, args.slice ? args.slice() : args ];
queue.push( args );
if ( !firing ) {
fire();
}
}
return this;
},
// call all the callbacks with the given arguments
fire: function() {
self.firewith( this, arguments );
return this;
},
// to know if the callbacks have already been called at least once
fired: function() {
return !!fired;
}
};
return self;
};
jquery.deferred对象jquery.deferred对象是一个工厂函数,返回一个用于异步或同步调用的deferred对象,支持链式调用、回调函数队列,并且能针对返回的状态不同执行不同的回调。它类似于es6提供的promise对象,提供9个主要的方法:
done: 操作成功响应时的回调函数(同步或异步,以下相同)
fail: 操作失败响应时的回调函数
progress: 操作处理过程中的回调函数
resolve: 通过该方法解析该操作为成功状态,调用done
reject: 通过该方法解析该操作为失败状态,调用fail
notify: 通过该方法解析该操作为执行过程中,调用progress
then: 设置回调的简写,接收三个参数,分别是done/fail/progress
always: 设置必须执行的回调,无论是done还是fail
promise: 返回一个受限制的deferred对象,不允许外部直接改变完成状态
它的实现思想是创建一个对象,包含不同状态下回调函数的队列,并在状态为失败或成功后不允许再次改变。通过返回的deferred对象进行手动调用resolve/reject/notify方法来控制流程。
看一个实例(纯属胡扯,不要当真)。我们需要从间谍卫星返回的数据用不同的算法来进行解析,如果解析结果信号强度大于90%,则证明该数据有效,可以被解析;如果强度小于10%,则证明只是宇宙噪音;否则,证明数据可能有效,换一种算法解析:
// 我们封装deferred产生一个promise对象,其不能被外部手动解析,只能内部确定最终状态
asynpromise = function () {
let d = $.deferred();
(function timer() {
settimeout(function () {
// 产生随机数,代替解析结果,来确定本次的状态
let num = math.random();
if (num > 0.9) {
d.resolve(); // 解析成功
} else if (num < 0.1) {
d.reject(); // 解析失败
} else {
d.notify(); // 解析过程中
}
settimeout(timer, 1000); // 持续不断的解析数据
}, 1000);
})();
// 如果不返回promise对象,则可以被外部手动调整解析状态
return d.promise();
};
// then方法的三个参数分别代表完成、失败、过程中的回调函数
asynpromise().then(function () {
console.log('resolve success');
}, function () {
console.log('reject fail');
}, function () {
console.log('notify progress');
});
// 本地执行结果(每个人的不一样,随机分布,但最后一个一定是success或fail)
notify progress
notify progress
notify progress
notify progress
notify progress
reject fail // 后面不会再有输出,因为一旦解析状态为success或fail,则不会再改变
除了上面的主要功能,还提供了notifywith/resolvewith/rejectwith/state辅助方法。
其所有的源码实现和注释为:
deferred: function( func ) {
var tuples = [
// action, add listener, callbacks,
// ... .then handlers, argument index, [final state]
// 用于后面进行第一个参数绑定调用第二个参数,第三个和第四个参数分别是其不同的回调函数队列
[ "notify", "progress", jquery.callbacks( "memory" ),
jquery.callbacks( "memory" ), 2 ],
[ "resolve", "done", jquery.callbacks( "once memory" ),
jquery.callbacks( "once memory" ), 0, "resolved" ],
[ "reject", "fail", jquery.callbacks( "once memory" ),
jquery.callbacks( "once memory" ), 1, "rejected" ]
],
state = "pending",
promise = {
state: function() {
return state;
},
// 同时添加done和fail句柄
always: function() {
deferred.done( arguments ).fail( arguments );
return this;
},
"catch": function( fn ) {
return promise.then( null, fn );
},
then: function( onfulfilled, onrejected, onprogress ) {
var maxdepth = 0;
function resolve( depth, deferred, handler, special ) {
return function() {
var that = this,
args = arguments,
mightthrow = function() {
var returned, then;
// support: promises/a+ section 2.3.3.3.3
// https://promisesaplus.com/#point-59
// ignore double-resolution attempts
if ( depth < maxdepth ) {
return;
}
returned = handler.apply( that, args );
// support: promises/a+ section 2.3.1
// https://promisesaplus.com/#point-48
if ( returned === deferred.promise() ) {
throw new typeerror( "thenable self-resolution" );
}
// support: promises/a+ sections 2.3.3.1, 3.5
// https://promisesaplus.com/#point-54
// https://promisesaplus.com/#point-75
// retrieve `then` only once
then = returned &&
// support: promises/a+ section 2.3.4
// https://promisesaplus.com/#point-64
// only check objects and functions for thenability
( typeof returned === "object" ||
typeof returned === "function" ) &&
returned.then;
// handle a returned thenable
if ( jquery.isfunction( then ) ) {
// special processors (notify) just wait for resolution
if ( special ) {
then.call(
returned,
resolve( maxdepth, deferred, identity, special ),
resolve( maxdepth, deferred, thrower, special )
);
// normal processors (resolve) also hook into progress
} else {
// ...and disregard older resolution values
maxdepth++;
then.call(
returned,
resolve( maxdepth, deferred, identity, special ),
resolve( maxdepth, deferred, thrower, special ),
resolve( maxdepth, deferred, identity,
deferred.notifywith )
);
}
// handle all other returned values
} else {
// only substitute handlers pass on context
// and multiple values (non-spec behavior)
if ( handler !== identity ) {
that = undefined;
args = [ returned ];
}
// process the value(s)
// default process is resolve
( special || deferred.resolvewith )( that, args );
}
},
// only normal processors (resolve) catch and reject exceptions
// 只有普通的process能处理异常,其余的要进行捕获,这里不是特别明白,应该是因为没有改最终的状态吧
process = special ?
mightthrow :
function() {
try {
mightthrow();
} catch ( e ) {
if ( jquery.deferred.exceptionhook ) {
jquery.deferred.exceptionhook( e,
process.stacktrace );
}
// support: promises/a+ section 2.3.3.3.4.1
// https://promisesaplus.com/#point-61
// ignore post-resolution exceptions
if ( depth + 1 >= maxdepth ) {
// only substitute handlers pass on context
// and multiple values (non-spec behavior)
if ( handler !== thrower ) {
that = undefined;
args = [ e ];
}
deferred.rejectwith( that, args );
}
}
};
// support: promises/a+ section 2.3.3.3.1
// https://promisesaplus.com/#point-57
// re-resolve promises immediately to dodge false rejection from
// subsequent errors
if ( depth ) {
process();
} else {
// call an optional hook to record the stack, in case of exception
// since it's otherwise lost when execution goes async
if ( jquery.deferred.getstackhook ) {
process.stacktrace = jquery.deferred.getstackhook();
}
window.settimeout( process );
}
};
}
return jquery.deferred( function( newdefer ) {
// progress_handlers.add( ... )
tuples[ 0 ][ 3 ].add(
resolve(
0,
newdefer,
jquery.isfunction( onprogress ) ?
onprogress :
identity,
newdefer.notifywith
)
);
// fulfilled_handlers.add( ... )
tuples[ 1 ][ 3 ].add(
resolve(
0,
newdefer,
jquery.isfunction( onfulfilled ) ?
onfulfilled :
identity
)
);
// rejected_handlers.add( ... )
tuples[ 2 ][ 3 ].add(
resolve(
0,
newdefer,
jquery.isfunction( onrejected ) ?
onrejected :
thrower
)
);
} ).promise();
},
// get a promise for this deferred
// if obj is provided, the promise aspect is added to the object
// 通过该promise对象返回一个新的扩展promise对象或自身
promise: function( obj ) {
return obj != null ? jquery.extend( obj, promise ) : promise;
}
},
deferred = {};
// add list-specific methods
// 给promise添加done/fail/progress事件,并添加互相的影响关系,并为deferred对象添加3个事件函数notify/resolve/reject
jquery.each( tuples, function( i, tuple ) {
var list = tuple[ 2 ],
statestring = tuple[ 5 ];
// promise.progress = list.add
// promise.done = list.add
// promise.fail = list.add
promise[ tuple[ 1 ] ] = list.add;
// handle state
// 只有done和fail有resolved和rejected状态字段,给两个事件添加回调,禁止再次done或者fail,锁住progress不允许执行回调
if ( statestring ) {
list.add(
function() {
// state = "resolved" (i.e., fulfilled)
// state = "rejected"
state = statestring;
},
// rejected_callbacks.disable
// fulfilled_callbacks.disable
tuples[ 3 - i ][ 2 ].disable,
// progress_callbacks.lock
tuples[ 0 ][ 2 ].lock
);
}
// progress_handlers.fire
// fulfilled_handlers.fire
// rejected_handlers.fire
// 执行第二个回调列表
list.add( tuple[ 3 ].fire );
// deferred.notify = function() { deferred.notifywith(...) }
// deferred.resolve = function() { deferred.resolvewith(...) }
// deferred.reject = function() { deferred.rejectwith(...) }
// 绑定notify/resolve/reject的事件,实际执行的函数体为加入上下文的with函数
deferred[ tuple[ 0 ] ] = function() {
deferred[ tuple[ 0 ] + "with" ]( this === deferred ? undefined : this, arguments );
return this;
};
// deferred.notifywith = list.firewith
// deferred.resolvewith = list.firewith
// deferred.rejectwith = list.firewith
deferred[ tuple[ 0 ] + "with" ] = list.firewith;
} );
// make the deferred a promise
// 将deferred扩展为一个promise对象
promise.promise( deferred );
// call given func if any
// 在创建前执行传入的回调函数进行修改
if ( func ) {
func.call( deferred, deferred );
}
// all done!
return deferred;
},
jquery.when方法$.when()提供一种方法执行一个或多个函数的回调函数。如果传入一个延迟对象,则返回该对象的promise对象,可以继续绑定其余回调,在执行结束状态之后也同时调用其when回调函数。如果传入多个延迟对象,则返回一个新的master延迟对象,跟踪所有的聚集状态,如果都成功解析完成,才调用其when回调函数;如果有一个失败,则全部失败,执行错误回调。
其使用方法:
$.when($.ajax("/page1.php"), $.ajax("/page2.php"))
.then(myfunc, myfailure);
其所有源码实现和注释为(能力有限,有些地方实在不能准确理解执行流程):
// 给when传递的对象绑定master.resolve和master.reject,用于聚集多异步对象的状态
function adoptvalue( value, resolve, reject, novalue ) {
var method;
try {
// check for promise aspect first to privilege synchronous behavior
// 如果when传入的参数promise方法可用,则封装promise并添加done和fail方法调用resolve和reject
if ( value && jquery.isfunction( ( method = value.promise ) ) ) {
method.call( value ).done( resolve ).fail( reject );
// other thenables
// 否则,就判断传入参数的then方法是否可用,如果可用就传入resolve和reject方法
} else if ( value && jquery.isfunction( ( method = value.then ) ) ) {
method.call( value, resolve, reject );
// other non-thenables
// 如果均不可用,则为非异步对象,直接resolve解析原值
} else {
// control `resolve` arguments by letting array#slice cast boolean `novalue` to integer:
// * false: [ value ].slice( 0 ) => resolve( value )
// * true: [ value ].slice( 1 ) => resolve()
resolve.apply( undefined, [ value ].slice( novalue ) );
}
// for promises/a+, convert exceptions into rejections
// since jquery.when doesn't unwrap thenables, we can skip the extra checks appearing in
// deferred#then to conditionally suppress rejection.
} catch ( value ) {
// support: android 4.0 only
// strict mode functions invoked without .call/.apply get global-object context
// 一个安卓4.0的bug,这里不做阐释
reject.apply( undefined, [ value ] );
}
}
// deferred helper
when: function( singlevalue ) {
var
// count of uncompleted subordinates
remaining = arguments.length,
// count of unprocessed arguments
i = remaining,
// subordinate fulfillment data
resolvecontexts = array( i ),
resolvevalues = slice.call( arguments ),
// the master deferred
master = jquery.deferred(),
// subordinate callback factory
// 将每一个响应的环境和值都保存到列表里,在全部完成后统一传给主promise用于执行
updatefunc = function( i ) {
return function( value ) {
resolvecontexts[ i ] = this;
resolvevalues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
if ( !( --remaining ) ) {
master.resolvewith( resolvecontexts, resolvevalues );
}
};
};
// single- and empty arguments are adopted like promise.resolve
// 如果只有一个参数,则直接将其作为master的回调
if ( remaining <= 1 ) {
adoptvalue( singlevalue, master.done( updatefunc( i ) ).resolve, master.reject,
!remaining );
// use .then() to unwrap secondary thenables (cf. gh-3000)
if ( master.state() === "pending" ||
jquery.isfunction( resolvevalues[ i ] && resolvevalues[ i ].then ) ) {
return master.then();
}
}
// multiple arguments are aggregated like promise.all array elements
// 多参数时,进行所有参数的解析状态聚合到master上
while ( i-- ) {
adoptvalue( resolvevalues[ i ], updatefunc( i ), master.reject );
}
return master.promise();
}
后续本来想把jquery.deferred和jquery.ajax以及es6的promise对象给统一讲一下,结果发现牵涉的东西太多,每一个都可以单独写一篇文章,怕大家说太长不看,这里先写第一部分jquery.deferred吧,后续再补充另外两篇。
看jquery的文档很容易,使用也很方便,但其实真正想要讲好很复杂,更不要说写篇源码分析文章了。真的是努力理解设计者的思路,争取每行都能理解边界条件,但踩坑太少,应用场景太少,确实有很大的疏漏,希望大家能够理解,不要偏听一面之词。
以上就是jquery源码解析deferred异步对象的方法的详细内容。