谈谈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 相关文章推荐
JavaScript 创建对象和构造类实现代码
Jul 30 Javascript
详解jQuery向动态生成的内容添加事件响应jQuery live()方法
Nov 02 Javascript
JS实现的自定义网页拖动类
Nov 06 Javascript
实例代码详解jquery.slides.js
Nov 16 Javascript
深入解析JavaScript中函数的Currying柯里化
Mar 19 Javascript
谈谈JavaScript中的几种借用方法
Aug 09 Javascript
Vue.js实现简单动态数据处理
Feb 13 Javascript
webpack配置文件和常用配置项介绍
Apr 28 Javascript
Vue slot用法(小结)
Oct 22 Javascript
基于Vue实现平滑过渡的拖拽排序功能
Jun 12 Javascript
浅谈目前可以使用ES10的5个新特性
Jun 25 Javascript
vue仿ios列表左划删除
Sep 26 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的特殊设置
2006/10/09 PHP
PHP中__get()和__set()的用法实例详解
2013/06/04 PHP
PHP实现的Redis多库选择功能单例类
2017/07/27 PHP
thinkphp5.0自定义验证规则使用方法
2017/11/16 PHP
thinkPHP5.1框架路由::get、post请求简单用法示例
2019/05/06 PHP
js读取本地excel文档数据的代码
2010/11/11 Javascript
jQuery版Tab标签切换
2011/03/16 Javascript
js和jquery对dom节点的操作(创建/追加)
2013/04/21 Javascript
js实现的点击数量加一可操作数据库
2014/05/09 Javascript
js 获取浏览器版本以此来调整CSS的样式
2014/06/03 Javascript
js拼接html注意问题示例探讨
2014/07/14 Javascript
基于BootStrap环境写jQuery tabs插件
2016/07/12 Javascript
表单元素值获取方式js及java方式的简单实例
2016/10/15 Javascript
Websocket协议详解及简单实例代码
2016/12/12 Javascript
AngularJS实现网站换肤实例
2021/02/19 Javascript
Angular.js中处理页面闪烁的方法详解
2017/03/09 Javascript
简单好用的nodejs 爬虫框架分享
2017/03/26 NodeJs
JS 设置Cookie 有效期 检测cookie
2017/06/15 Javascript
BootStrap selectpicker后台动态绑定数据的方法
2017/07/28 Javascript
Thinkjs3新手入门之如何使用静态资源目录
2017/12/06 Javascript
vue构建动态表单的方法示例
2018/09/22 Javascript
小程序实现横向滑动日历效果
2019/10/21 Javascript
Python利用带权重随机数解决抽奖和游戏爆装备问题
2016/06/16 Python
python3安装pip3(install pip3 for python 3.x)
2018/04/03 Python
详解pyppeteer(python版puppeteer)基本使用
2019/06/12 Python
Python写出新冠状病毒确诊人数地图的方法
2020/02/12 Python
用于ETL的Python数据转换工具详解
2020/07/21 Python
使用jupyter notebook运行python和R的步骤
2020/08/13 Python
Python configparser模块应用过程解析
2020/08/14 Python
python中watchdog文件监控与检测上传功能
2020/10/30 Python
Scrapy实现模拟登录的示例代码
2021/02/21 Python
印度婴儿用品在线商店:Firstcry.com
2016/12/05 全球购物
毕业生简历自我评价范文
2014/04/09 职场文书
小学教师求职信范文
2015/03/20 职场文书
Python3的进程和线程你了解吗
2022/03/16 Python
Java结构型设计模式之组合模式详解
2022/09/23 Java/Android