谈谈jQuery之Deferred源码剖析


Posted in Javascript onDecember 19, 2016

一、前言

大约在夏季,我们谈过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之Deferred源码剖析

它是个什么玩意儿?

jQuery.Callbacks = function( options ) {

  // Convert options from String-formatted to Object-formatted if needed
  // (we check in cache first)
  options = typeof options === "string" ?
    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 = options.once;

      // Execute callbacks for all pending executions,
      // respecting firingIndex overrides and runtime changes
      fired = firing = true;
      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
      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
      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
      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 = 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 ? 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.Callbacks源码,如果你了解设计模式中的发布订阅者模式,不难发现,就是一个”自定义事件”嘛

所以,我们精简jQuery.Callbacks后,核心代码如下:

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方法的对象,每个方法详解如下:

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 ? jQuery.extend( obj, promise ) : promise;
  }
}

随后声明的一个空对象deferred。

promise.pipe=promise.then,就不累赘了,下面我们来看看jQuery.each(tuples, function(i, tuple){…})都干了什么,源码如下:

/*
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 ? promise : this, arguments );
    return this;
  };
  deferred[ tuple[ 0 ] + "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对象如下图所示:

 谈谈jQuery之Deferred源码剖析

我们在前面讲解promise对象时,提到过它的promise属性,即为扩展promise对象,再回顾下:

 谈谈jQuery之Deferred源码剖析

所以接下来,源代码中的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 ? newDefer.promise() : this,
              fn ? [ returned ] : arguments
            );
          }
        } );
      } );
      fns = null;
    } ).promise();
  }
}

精简promise.then的源码如下:

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作为执行对象和参数传入么: 

谈谈jQuery之Deferred源码剖析

固,promise.then方法中的newDefer指向通过jQuery.Deferred构建好的deferred。

紧接着,jQuery.each(tuples, function(i,tuple){…})处理,重点就是deferred[tuple[1]](function(){…});,注意,这里的deferred是then方法的父deferred哦,如下:

谈谈jQuery之Deferred源码剖析

且tuple[1]为—done|fail|progress,在前面我们已经谈过,它们指向各自自定义事件对象的add方法。因此,也就明白了为什么deferred.resolve|reject|notify后,如果随后有then,会触发then方法的相关事件,如下:

谈谈jQuery之Deferred源码剖析

但是,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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
关于图片验证码设计的思考
Jan 29 Javascript
js innerHTML 的一些问题的解决方法
Jun 22 Javascript
Javascript 作用域使用说明
Aug 13 Javascript
动态加载js和css(外部文件)
Apr 17 Javascript
移动节点的jquery代码
Jan 13 Javascript
JS生成随机字符串的多种方法
Jun 10 Javascript
不用一句js代码初始化组件
Jan 27 Javascript
jquery表单验证插件validation使用方法详解
Jan 20 Javascript
Angular+Node生成随机数的方法
Jun 16 Javascript
Vue实现微信支付功能遇到的坑
Jun 05 Javascript
react 不用插件实现数字滚动的效果示例
Apr 14 Javascript
element 动态合并表格的步骤
Dec 31 Javascript
15个非常实用的JavaScript代码片段
Dec 18 #Javascript
scroll事件实现监控滚动条并分页显示(zepto.js)
Dec 18 #Javascript
简单实现node.js图片上传
Dec 18 #Javascript
Javascript计算二维数组重复值示例代码
Dec 18 #Javascript
Jquery Easyui选项卡组件Tab使用详解(10)
Dec 18 #Javascript
Jquery Easyui菜单组件Menu使用详解(15)
Dec 18 #Javascript
node.js请求HTTPS报错:UNABLE_TO_VERIFY_LEAF_SIGNATURE\的解决方法
Dec 18 #Javascript
You might like
php set_magic_quotes_runtime() 函数过时解决方法
2010/07/08 PHP
浅析PHP中的字符串编码转换(自动识别原编码)
2013/07/02 PHP
PHP批量生成静态HTML的简单原理和方法
2014/04/20 PHP
php简单判断文本编码的方法
2015/07/30 PHP
Yii2中使用join、joinwith多表关联查询
2016/06/30 PHP
php mysql_list_dbs()函数用法示例
2017/03/29 PHP
Ajax请求PHP后台接口返回信息的实例代码
2018/08/21 PHP
PHP tp5中使用原生sql查询代码实例
2020/10/28 PHP
键盘 keycode的值 javascript时触发事件时很有用的要素
2009/11/02 Javascript
菜鸟javascript基础资料整理3 正则
2010/12/06 Javascript
setTimeout自动触发一个js的方法
2014/01/15 Javascript
浅谈Unicode与JavaScript的发展史
2015/01/19 Javascript
详解Node.Js如何处理post数据
2016/09/19 Javascript
jQuery实现右键菜单、遮罩等效果代码
2016/09/27 Javascript
javascript中this关键字详解
2016/12/12 Javascript
原生JS实现圆环拖拽效果
2017/04/07 Javascript
javascript中mouseenter与mouseover的异同
2017/06/06 Javascript
JavaScript中document.referrer的用法详解
2017/07/04 Javascript
axios拦截设置和错误处理方法
2018/03/05 Javascript
微信小程序如何使用云开发
2019/05/17 Javascript
[38:41]2014 DOTA2国际邀请赛中国区预选赛 LGD VS CNB
2014/05/22 DOTA
用Python中的wxPython实现最基本的浏览器功能
2015/04/14 Python
给Python入门者的一些编程建议
2015/06/15 Python
基于Python Shell获取hostname和fqdn释疑
2016/01/25 Python
Jupyter安装nbextensions,启动提示没有nbextensions库
2020/04/23 Python
pytorch 调整某一维度数据顺序的方法
2018/12/08 Python
Python完全识别验证码自动登录实例详解
2019/11/24 Python
python实现超级玛丽游戏
2020/03/18 Python
详解pandas中利用DataFrame对象的.loc[]、.iloc[]方法抽取数据
2020/12/13 Python
巴西最大的家具及装饰用品店:Mobly
2017/10/11 全球购物
波兰运动鞋网上商店:e-Sporting
2018/02/16 全球购物
正宗的日本零食和糖果订阅盒:Bokksu
2019/11/21 全球购物
幼儿园元旦亲子活动方案
2014/02/17 职场文书
村班子对照检查材料
2014/08/18 职场文书
python引入其他文件夹下的py文件具体方法
2021/05/23 Python
Pytorch中的学习率衰减及其用法详解
2021/06/05 Python