谈谈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 相关文章推荐
根据json字符串生成Html的一种方式
Jan 09 Javascript
js实现简单鼠标跟随效果的方法
Apr 10 Javascript
详解JavaScript中Date.UTC()方法的使用
Jun 12 Javascript
详解JavaScript对Date对象的操作问题(生成一个倒数7天的数组)
Oct 01 Javascript
jQuery中inArray方法注意事项分析
Jan 25 Javascript
Js动态设置rem来实现移动端字体的自适应代码
Oct 14 Javascript
js 单引号替换成双引号,双引号替换成单引号的实现方法
Feb 16 Javascript
JS实现的加减乘除四则运算计算器示例
Aug 09 Javascript
基于javascript中的typeof和类型判断(详解)
Oct 27 Javascript
微信小程序picker组件简单用法示例【附demo源码下载】
Dec 05 Javascript
使用Bootstrap4 + Vue2实现分页查询的示例代码
Dec 21 Javascript
jquery 通过ajax请求获取后台数据显示在表格上的方法
Aug 08 jQuery
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
解析Ubuntu下crontab命令的用法
2013/06/24 PHP
php根据一个给定范围和步进生成数组的方法
2015/06/19 PHP
CI框架整合smarty步骤详解
2016/05/19 PHP
浅谈Laravel中的三种中间件的作用
2019/10/13 PHP
4种Windows系统下Laravel框架的开发环境安装及部署方法详解
2020/04/06 PHP
PHP预定义接口――Iterator用法示例
2020/06/05 PHP
对联广告js flash激活
2006/10/19 Javascript
JavaScript Date对象 日期获取函数
2010/12/19 Javascript
通过JS动态创建一个html DOM元素并显示
2014/10/15 Javascript
node.js中的buffer.Buffer.isBuffer方法使用说明
2014/12/14 Javascript
jquery实现华丽的可折角广告代码
2015/09/02 Javascript
AngularJS实现全选反选功能
2015/12/08 Javascript
关于Vue.js一些问题和思考学习笔记(1)
2016/12/02 Javascript
json数据处理及数据绑定
2017/01/25 Javascript
史上最全JavaScript常用的简写技巧(推荐)
2017/08/17 Javascript
深入了解JavaScript 私有化
2019/05/30 Javascript
VUE注册全局组件和局部组件过程解析
2019/10/10 Javascript
通过vue刷新左侧菜单栏操作
2020/08/06 Javascript
python根据给定文件返回文件名和扩展名的方法
2015/03/27 Python
点球小游戏python脚本
2018/05/22 Python
配置python的编程环境之Anaconda + VSCode的教程
2020/03/29 Python
Python rabbitMQ如何实现生产消费者模式
2020/08/24 Python
JRE、JDK、JVM之间的关系怎样
2012/05/16 面试题
介绍一下Prototype的$()函数,$F()函数,$A()函数都是什么作用?
2014/03/05 面试题
UNIX文件名称有什么规定
2013/03/25 面试题
计算机专业学生求职信分享
2013/12/15 职场文书
经理助理岗位职责
2014/03/05 职场文书
GMP办公室主任岗位职责
2014/03/14 职场文书
机关出纳岗位职责
2014/04/03 职场文书
工商管理专业毕业生求职信
2014/05/26 职场文书
会计专业自荐书
2014/07/08 职场文书
学习十八大的心得体会
2014/09/01 职场文书
2016年春季运动会广播稿
2015/08/19 职场文书
《分数的意义》教学反思
2016/02/20 职场文书
2016优秀班主任个人先进事迹材料
2016/02/26 职场文书
2016年“我们的节日·清明节”活动总结
2016/04/01 职场文书