谈谈jQuery之Deferred源码剖析
一、前言
大约在夏季,我们谈过ES6的Promise,其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样,通过链式调用,避免层层嵌套,如下:
//jquery版本大于1.8 function runAsync(){ var def = $.Deferred(); setTimeout(function(){ console.log('I am done'); def.resolve('whatever'); }, 1000); return def; } runAsync().then(function(msg){ console.log(msg);//=>打印'whatever' }).done(function(msg){ console.log(msg);//=>打印'undefined' });
注:从jQuery1.8版本开始,then方法会返回一个新的受限制的deferred对象,即deferred.promise()—后续源码解读中我们会更加全面地了解到。因此,上述代码done中会打印'undefined'。
好了,通过上述示例代码,短暂的回顾了jQuery的Deferred使用后,我们一起来看看jQuery是怎么实现Deferred,当然解读jQuery的版本是大于1.8。
二、jQuery之Deferred源码剖析
整体架构,如下:
jQuery.extend( { 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", promise = { state: function() {...}, always: function() {...}, then: function() {...}, promise: function( obj ) {...} }, deferred = {}; // Keep pipe for back-compat promise.pipe = promise.then; // Add list-specific methods jQuery.each( tuples, function( i, tuple ) {} ); // Make the deferred a promise promise.promise( deferred ); // Call given func if any if ( func ) { func.call( deferred, deferred ); } // All done! return deferred; } }
整体架构上,如果你了解设计模式中的工厂模式,那么不难看出,jQuery.Deferred就是一个工厂,每次执行jQuery.Deferred时,都会返回一个加工好的deferred对象。
接下来,我们再一步一步剖析上述代码。
首先,是数组tuples:
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" ) ] ]
tuples一开始就为我们预先定义了三种状态—‘resolved'、'rejected'以及'pending',以及它们所对应的一系列值和操作,值得注意的是每种状态中,都调用了一个jQuery.Callbacks方法,如下:
它是个什么玩意儿?
jQuery.Callbacks = function( options ) { // Convert options from String-formatted to Object-formatted if needed // (we check in cache first) options = typeof options === "string" ""; } } }, // 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 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 ""; 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 = true; if ( !memory ) { self.disable(); } return this; }, locked: function() { return !!locked; }, // Call all callbacks with the given context and arguments fireWith: function( context, args ) { if ( !locked ) { args = args || []; args = [ context, args.slice "htmlcode">jQuery.Callbacks = function(){ var list = [], self = { add: function(){/*添加元素到list*/}, remove: function(){/*从list移除指定元素*/}, fire: function(){/*遍历list并触发每次元素*/} }; return self; }一目了然,我们每执行一次jQuery.Callbacks方法,它就会返回一个独立的自定义事件对象。在tuples每个状态中执行一次jQuery.Callbacks,也就豁然开朗了—为每个状态提供一个独立的空间来添加、删除以及触发事件。
好了,关于变量tuples,我们就算大致解读完了。
state就是deferred对象的状态值嘛,我们可以通过deferred.state方法获取(稍后会见到)。
promise就是一个拥有state、always、then、promise方法的对象,每个方法详解如下:
上一篇:javascript中call,apply,bind函数用法示例promise = { state: function() {//返回状态值 return state; }, always: function() {//不管成功还是失败,最终都会执行该方法 deferred.done( arguments ).fail( arguments ); return this; }, then: function( /* fnDone, fnFail, fnProgress */ ) {...},//重头戏,稍后会详讲 promise: function( obj ) {//扩展promise,如不久我们会看见的promise.promise( deferred ); return obj != null "htmlcode">/* tuples = [ [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], [ "notify", "progress", jQuery.Callbacks( "memory" ) ] ] */ jQuery.each( tuples, function( i, tuple ) { var list = tuple[ 2 ], stateString = tuple[ 3 ]; // promise[ done | fail | progress ] = list.add promise[ tuple[ 1 ] ] = list.add; // Handle state if ( stateString ) { 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 ); } // deferred[ resolve | reject | notify ] deferred[ tuple[ 0 ] ] = function() { deferred[ tuple[ 0 ] + "With" ]( this === deferred "With" ] = list.fireWith; } );通过jQuery.each遍历tuples数组,并对其进行相关操作,比如我们拿tuples数组中的第一个元素举例:
['resolve', 'done', jQuery.Callbacks('once memory'), 'resolved']
第一步、声明的变量list指向jQuery.Callbacks返回的对象,stateString取值为'resolved'
第二步、为promise添加'done'属性,并指向第一步中list.add(fail和progress即指向属于各自的自定义事件对象)
第三步、判断stateString值,如果为'resolved'或'rejected'状态,那么就添加三个事件函数到对应的list列表中:
- --改变state状态的函数
- --禁止对应状态的处理,如'resolved'后,那么必定不会触发rejected状态咯,反之亦然
- --禁止pending状态,都'resolved'或者'rejected'了,那么deferred肯定不会处于pending状态咯
第四步、为对象deferred,添加触发各自状态('resolved','rejected','pending')的fire相关方法:
- --resolve、resolveWith
- --reject、rejectWith
- --notify、notifyWith
好了,jQuery.each(tuples, function(i, tuple){…})解读就到此结束了。
总结:
通过jQuery.each遍历tuples,将tuples里的三种状态操作值done、fail以及progress添加到promise对象,并分别指向各自自定义对象中的add方法。如果状态为resolved或rejected,那么,再将三个特定函数添加到各自自定义对象的list列表下。随后,就是对deferred对象赋予三个状态各自的触发事件啦。
至此,promise、deferred对象如下图所示:
我们在前面讲解promise对象时,提到过它的promise属性,即为扩展promise对象,再回顾下:
所以接下来,源代码中的promise.promise(deferred),即为扩展deferred对象,让原来只有6个触发属性的deferred,同时拥有了promise对象的全部属性。
紧接着,func.call(deferred, deferred),即为执行参数func,当然,前提是func有值。值得注意的是,是将deferred作为func的执行对象以及执行参数的,这一点在promise.then中体现得淋淋尽致(稍后会细说)。
最后$.Deferred返回构建好的deferred对象。
到此,构建deferred整体流程走完。
三、细说promise.then
promise.then源码如下:
promise = { then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; return jQuery.Deferred( function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; // deferred[ done | fail | progress ] for forwarding actions to newDefer deferred[ tuple[ 1 ] ]( function() { var returned = fn && fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { returned.promise() .progress( newDefer.notify ) .done( newDefer.resolve ) .fail( newDefer.reject ); } else { newDefer[ tuple[ 0 ] + "With" ]( this === promise "htmlcode">promise = { then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; return jQuery.Deferred( function( newDefer ) { ... } ).promise(); } }整体架构上,可以清晰的看出,promise.then方法最后通过jQuery.Deferred返回了一个新的受限制的deferred对象,即deferred.promise,正因为这样,所以执行完then方法后,我们是不能通过deferred.pomise手动触发resolve、reject或notify的。
接下来,我们再一步一步剖析promise.then源码。
var fns = arguments不过就是将then方法中的参数赋予fns,在接下来的jQuery.each里使用。接着,就通过jQuery.Deferred返回了一个构建好的deferred对象,但是注意,在jQuery.Deferred里有个参数—匿名函数,还记得在上一小节末尾处,我们说过如果jQuery.Deferred里有值,就执行它,并将构建好的deferred作为执行对象和参数传入么:
固,promise.then方法中的newDefer指向通过jQuery.Deferred构建好的deferred。
紧接着,jQuery.each(tuples, function(i,tuple){…})处理,重点就是deferred[tuple[1]](function(){…});,注意,这里的deferred是then方法的父deferred哦,如下:
且tuple[1]为—done|fail|progress,在前面我们已经谈过,它们指向各自自定义事件对象的add方法。因此,也就明白了为什么deferred.resolve|reject|notify后,如果随后有then,会触发then方法的相关事件,如下:
但是,then方法后有then方法,又是怎么操作的呢?
它会判断then方法中的回调函数的返回值,如果是一个deferred对象,那么就将then方法自行创建的deferred对象中的相关触发事件,添加到回调函数中返回的deferred对象的对应的list列表中,这样,当我们触发回调函数中的相关触发事件后,也就会触发then方法的deferred对象了,从而,如果then方法后有then方法,也就关联了。
好了,那么如果then方法中的回调函数的返回值是一个非deferred对象呢?那么它就将这个返回值带上,直接触发then方法自行创建的deferred对象的相关事件,从而,如果then方法后有then方法,也就关联了。
好了,promise.then方法解决就算基本完毕。
四、思考
细细品来,大家有没有发现,其实promise.then就是通过作用域链,利用jQuery.Deferred中的变量deferred来关联父deferred的。如果,你还记得数据结构中的单链表,有没有发觉似曾相识呢,作者在这里通过jQuery.Deferred这个工厂构建每个deferred,然后利用作用域链相互关联,就如同单链表一样。
因此,借助这一思想,我们就一同模拟一个非常简单的Deferred,称作SimpleDef。主要作用就是每次我们执行SimpleDef函数,它都会返回一个构建好的simpleDef对象,该对象里面包含了三个方法done、then以及fire:
- --done就如同add方法般,将done里的参数添加到它父simpleDef列表list中,并返回父simpleDef对象;
- --then就是将其参数func添加到父SimpleDef对象的列表list中,并返回一个新SimpleDef对象;
- --fire就是触发对应simpleDef对象的list列表里的所有函数。
实现代码如下:
function SimpleDef(){ var list = [], simpleDef = { done: function(func){ list.push(func); return simpleDef; }, then: function(func){ list.push(func); return SimpleDef(); }, fire: function(){ var i = list.length; while(i--){ list[i](); } } }; return simpleDef; }测试代码如下:
var def = SimpleDef(); var then1 = def.done(function(){ console.log('self1-done1'); }).done(function(){ console.log('self1-done2'); }).then(function(){ console.log('self2-then1'); }).done(function(){ console.log('self2-done1'); }); def.fire();//=>self2-then1 self1-done2 self1-done1 console.log('xxxxxxxxxxxxxxxxxxxx'); then1.fire();//=>self2-done1以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
下一篇:15个非常实用的JavaScript代码片段
人们对于笔记本电脑有一个固有印象:要么轻薄但性能一般,要么性能强劲但笨重臃肿。然而,今年荣耀新推出的MagicBook Pro 16刷新了人们的认知——发布会上,荣耀宣布猎人游戏本正式回归,称其继承了荣耀 HUNTER 基因,并自信地为其打出“轻薄本,更是游戏本”的口号。
众所周知,寻求轻薄本的用户普遍更看重便携性、外观造型、静谧性和打字办公等用机体验,而寻求游戏本的用户则普遍更看重硬件配置、性能释放等硬核指标。把两个看似难以相干的产品融合到一起,我们不禁对它产生了强烈的好奇:作为代表荣耀猎人游戏本的跨界新物种,它究竟做了哪些平衡以兼顾不同人群的各类需求呢?