jQuery.Deferred 对象源码解析

Deferred 是什么?

实际上 deferred 就是 Promise/A+ 标准的 jQuery 的实现,是一个管理异步操作的对象

jQuery.Deferred 大体逻辑

  • 源码大概代码如下
jQuery.extend(
  Deferred: function( func ) {
    // 声明各个状态的 状态转移方法、回调注册方法、回调列表对象、状态名等
    var tuples = [
      [ ... ], // 对应 fulfilled
      [ ... ], // 对应 rejected
      [ ... ]  // 对应 progress 
    ], 
      // 初始状态声明,即 等待 状态
      state = "pending", 
      
      // promise 对象,用于处理异步回调,如 done()、fail()、progress()、then() 等,是 deferred 的一个子集
      promise = { ... },
      
      // deferred 对象,声明时就是一个空对象,除了能处理异步回调,还能更改 promise 状态
      deferred = {}; 
    
    // 向 deferred 和 promise 里注入 done,fail,progress 等回调注册方法
    // 向 deferred 里注入 resolve、reject、notify 等更改状态的方法
    jQuery.each( tuples, function( tuple, i ) {
      ...
    } );
    
    // 将 promise 里的对象混合到 deferred 中
    promise.promise( deferred );

    // Deferred() 实际上是个工厂函数,func 可用于配置 deferred,常用于多级异步处理
    if ( func ) {
      func.call( deferred, deferred )
    }

    // 最后返回 deferred 对象
    return deferred;
  }
)

总结:

  1. 声明 tuples、state、promise、deferred 变量;
  2. 将 tuples 混入 deferred 中;
  3. 将 promise 混入 deferred 中;
  4. 如果有对 deferred 的配置函数 func,则执行;
  5. 最后返回 deferred 对象;

作为配置数组的 tuples

  • 来看 tuples 的具体定义
// tuples 包含三个数组,对应了 deferred 的三种状态,每个 tuple 是一种状态
// tuple[ 0 ] 是状态转移方法名
// tuple[ 1 ] 是回调注册方法名
// tuple[ 2 ] 是通过 tuple[ 1 ] 方法注册的回调函数的容器,其中 resolve 和 reject 是单次执行的
// tuple[ 3 ] 是通过 then() 方法注册的回调函数的容器
// tuple[ 4 ] 只用在 then() 方法的定义中,是用于标明回调函数类型的下标
// tuple[ 5 ] 是最终状态
var tuples = 
  [ 
    [ "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" ]
  ]

总结:
该数组实际上对整个 deferred 的 基本方法名、状态名、回调管理容器 进行了定义。


promise 对象定义

  • promise 对象是 deferred 中非常关键的一个对象,它是 deferred 的子集,定义了一系列的回调注册的函数,声明时有如下成员对象:
promise = {
  
  // 返回当前状态
  state: function() {
    return state;
  },

  // 注入 onFulFilled 或 onRejected 都要调用的回调函数
  always: function() {
    deferred.done( arguments ).fail( arguments );
    return this;
  },

  // catch 语法糖,实际就是通过 then 注入 onRejected 回调
  "catch": function() {
    return promise.then( null, fn );
  },
  pipe: function() { ... },
  then: function() { ... },
  promise: function() { ... }
}

  • 下面详解 pipe 的实现
    pipe 可以理解为一个钩子函数,传参为三个回调,对状态返回结果进行预处理。
    源码如下:
pipe: function( /* fnDone, fnFail, fnProgress */ ) {

  var fns = arguments;

  // 返回的是一个新的 deferred 对象,通过配置函数进行了改造
  // 向新的 deferred 的回调列表加入了回调函数
  return jQuery.Deffered( function( newDefer ) {

    jQuery.each( tuples, function( i, tuple ) {
      
      // 通过 tuple[ 4 ] 下标获取参数中对应的回调函数 fn
      var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];
      
      // 在对应的回调列表中注入回调函数
      deferred[ tuple[ 1 ] ]( function() {
        
        // 先调用获取的 fn 得到 returned
        var returned = fn && fn.call( this, arguments );

        // returned 可能是一个 promise 或者 deferred,都有 promise 方法
        // 这种情况下,返回的新的 deferred 的状态改变应该依赖于 returned 的状态改变
        // 上一条规则参照 Promise/A+ 规范
        if ( returned && isFunction( returned.promise ) ) {
          
          // 于是就将新的 deferred 的状态转移函数注入到 returned 的回调列表中
          returned.promise()
            .progress( newDefer.notify )
            .done( newDefer.resolve )
            .fail( newDefer.reject )
        } else {
          
          // 否则,returned 就应该作为状态转移函数的入参
          // 注意,这里 this 继承了该 deferred 状态转移函数传入的上下文对象
          // 如果没有 fn,参数就为该 deferred 状态转移函数传入的参数
          newDefer[ tuple[ 0 ] + 'With' ](
            this,
            fn ? [ returned ] : arguments
          );
        }
      } );
    } );

    fns = null;
  } ).promise();
}

总结:

  1. pipe 函数用传入的回调对原来的 deferred 进行了装饰,返回一个新的 deferred 对象;
  2. 如果回调的返回值也是一个异步对象,如 promise 或 deferred,那么新的 deferred 的状态依赖于这个返回值的状态;
  3. 如果返回值是一个非异步对象,就直接作为新 deferred 的状态转移函数的参数;
  4. 新 deferred 的状态转移函数的上下文继承老的 deferred 的状态转移函数的上下文;
  5. 如果未传入回调,新 deferred 解析的结果就是老 deferred 的状态转移函数入参;

  • 下面详解 then 方法的实现
    then 方法是 promise 标准的核心方法,也是最复杂的一个,先来看代码:
then: function( onFulfilled, onReject, onProgress ) {

  // 嵌套多层异步行为时的深度标识
  var maxDepth = 0;
  
  // 返回一个解析方法,通过传入的参数进行配置
  function resolve( depth, deferred, handler, special ) {
    // 核心解析方法,在回调中实际执行的方法
    return function() {
      ...
    }
  }
  
  // 返回一个新的 deferred 对象,并向其回调列表中注入解析方法
  return jQuery.Deferred( function( newDefer ) {
    
     // 可以看出,用 then 方法的三个入参来配置解析函数,并注入相应的回调列表中
     // 而且注意到,then 注入的回调是放在 tuple[ 3 ] 里
     // 而之前 done、fail、progress 注入的回调是放在 tuple[ 2 ] 里
     tuples[ 0 ][ 3 ].add(
        resolve(
          0,
          newDefer,
          isFunction( onProgress ) ?
            onProgress :
            Indentity
        )
     );

     tuples[ 1 ][ 3 ].add(
        resolve(
          0,
          newDefer,
          isFunction( onFulfilled ) ?
            onFulfilled :
            Indentity
        )
     );

     tuples[ 2 ][ 3 ].add(
        resolve(
          0,
          newDefer,
          isFunction( onRejected ) ?
            onRejected :
            Thrower
        )
     );

  } ).promise()
}

下面具体来看一下 resolve 解析方法的实现:

function reslove( depth, deferred, handler, specail ) {
  return function() {
    
    // 调用方法时,如果传入了上下文对象,比如用 resolveWith 更改方法时
    // 就将上下文赋予 that,以便在 mightThrow 函数中调用 
    var that = this,
      args = arguments,
      
      mightThrow = function() {
        var returned, then;
        
        // 如果 depth < maxDepth,说明在 depth 这个深度的 deferred 已经被解析过了,不允许重复解析
        if ( depth < maxDepth ) {
          return;
        }
        
        returned = handler.apply( that, args );
        
        // 如果返回值是 deferred 自身的 promise,需要抛出异常
        // 否则会将 deferred 的状态转移函数注入自身的回调列表中,造成自己依赖于自己的矛盾
        if ( returned === deferred.promise() ) {
          throw new TypeError( "Thenable self-resolution" );
        }
        
        // 如果 returned 可能是一种 promise 实现,则将 returned.then 赋予 then 变量 
        then = returned &&
          ( typeof returned === "object" ||
            typeof returned === "function" ) &&
          returned.then;
        
        // 如果 then 是 function,可以确定 returned 就是一种 promise 实现
        if ( isFunction( then ) ) {

          // 如果有 special,说明已经指定了 onProgress 回调,无需再由 resolve 生成
          // 而且该 function 是注入到 progress 回调列表中,不影响 maxDepth,即不阻止 notify 调用
          if ( special ) {
            then.call(
              returned,
              resolve( maxDepth, deferred, Indentity, special ),
              resolve( maxDepth, deferred, Thrower, special )   
            );
          } else {
            
            // 否则,该 function 必然是注入 done 回调中,最后一个参数的 special 传入了 deferred.notifyWith
            maxDepth++;
            then.call(
              returned,
              resolve( maxDepth, deferred, Indentity, special ),
              resolve( maxDepth, deferred, Thrower, special ),
              resolve( maxDepth, deferred, Indentity, deferred.notifyWith )
            );
          }
        } else {
          
          // 如果 then 不是 function,则说明 returned 就是要返回的 value 值
          // 如果 handler 不是 Indentity,那 handler 必然是自定义的回调
          // 则该异步解析函数的深度为 0,原来的 deferred 状态转移时回调执行的上下文不会影响新返回的 deferred 回调执行的上下文
          if ( handler !== Indentity ) {
            that = undefined;
            args = [ returned ];
          }
          
          // 如果 special 存在,就应该是 notifyWith 方法,直接调用即可
          // 否则 handler 执行时未抛出异常,说明状态是 fulfilled,调用 deferred.resolveWith 方法即可
          ( special || deferred.resolveWith )( that, args );

        }
      },

      // special 存在即为 notify 调用,不会抛出异常,直接调用 mightThrow 即可
      // 否则可能是 resolved 或 rejected,需要捕获异常
      process = special ? 
        mightThrow :
        function() {
          try{
            mightThrow()
          } catch( e ) {
            if ( jQuery.Deferred.exceptionHook ) {

              // 在控制台中显示异常信息
              jQuery.Deferred.exceptionHook( e, process.stackTrace )
            }

            // 在已经解析过的深度发生的任何异常都忽略掉
            if ( depth + 1 >= maxDepth ) {
              if ( handler !== Thrower ) {
                that = undefined;
                args = [ e ];
              }

              // 说明该异步对象已经被完全解析,此时捕获的异常就是 reject 的参数
              // 当前解析的深度 >= maxDepth
              deferred.rejectWith( that, args );
            }
          }
        };

    // 如果 depth 大于 0,则直接执行 process
    if ( depth ) {
      process();
    } else {
      if ( jQuery.Deferred.getStackHook ) {
        process.strackTrace = jQuery.Deferred.getStackHook();
      }

      // 否则设置异步执行
      window.setTimeout( process );
    }
  };
}

关于 resolve 的深度,这里举几个例子:

let defer = $.Deferred();
let defer2 = $.Deferred();

let newDefer = defer.then(
  function() {
    console.log( 'defer resolved' )
    return defer2;
  },
  function() {
    console.log( 'defer rejected' )
  },
  function() {
    console.log( 'defer notified ')
  }
)

// defer resolve 后,因为回调中有异步嵌套
// 深度为 0 的异步对象 defer 解析成功
// 等待深度为 1 的异步对象进行解析
// 执行该歩后打印:defer is resolved
defer.resolve();

// 此时触发的异步操作深度为 0,而此时 maxDepth = 1,所以对应回调不会执行
// 执行该歩后不会打印任何东西
// 注意:defer 中注册的 onProgress 不执行是应为 defer.resolve() 将其回调锁住了
// 而 newDefer 的 onProgress 不执行才是因为 depth 而被忽略了
defer.notify();

// 因为将 newDefer 的状态更变权交给了 defer2,所以会导致 newDefer 的 onProgress 回调触发
// 打印:newDefer is notified
defer2.notify();

let defer3 = $.Deferred();

// 同时,defer2 解析后入参为 defer3,也会形成异步嵌套,此时 maxDepth = 2
// newDefer 将会等待 defer3 的状态改变
defer2.resolve( defer3 );

// 因为 defer2 已经解析过
// 深度 depth = 1 < maxDepth,
// 所以不会打印任何东西
defer2.notify()

// defer3 解析完成,导致 newDefer 状态变为 fulfilled
// 打印:newDefer is resolved
defer3.resolve()

总结:

  1. then 注入回调能支持深层的异步嵌套,而 done、fail 不能,且在所属异步对象的状态解析完成后执行;
  2. then 只是确定异步的时序关系,并不保证每个异步对象最终状态是否达到,这点从语义上也能感觉到;

下面介绍通过 jQuery.each 将 tuples 注入 promise 和 deferred 中

直接看源码和注释就行:

jQuery.each( tuples, function( i, tuple ) {

  // list 即为 done、fail、progress 注入的回调函数列表
  var list = tuple[ 2 ],
    // 状态字符串
    stateString = tuple[ 5 ];
  
  // 将回调注册方法直接赋给 done、fail、progress
  promise[ tuple[ 1 ] ] = list.add;
  
  // 对于解析状态的数组
  if ( stateString ) {
    list.add(
      
      // 更改状态值
      function() {
        state = stateString;
      },
      
      // 废弃另一个状态的回调列表(done、fail、progress)注册的回调列表
      tuples[ 3 - i ][ 2 ].disable,

      // 废弃另一个状态 then 注册的回调列表
      tuples[ 3 - i ][ 3 ].disable,
      
      // 锁定 progress 的回调列表
      tuples[ 0 ][ 2 ].lock,
      tuples[ 0 ][ 3 ].lock
    );
  }

  // 将 then 的回调列表的 fire 方法注册进 [ done, fail, progress ] 的回调列表中
  // 所以 then 注册的回调总是在 onDone、onFail、onProgress 之后触发
  list.add( tuple[ 3 ].fire );
  
  // 向 deferred 里注入状态转移方法,在这里 this 不能是自身
  deferred[ tuple[ 0 ] ] = function() {
    deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments );
    return this;
  };

  // 向 deferred 里注入带上下文的状态转移方法
  deferred[ tuple[ 0 ] + "With" ] = list.fireWith;

} );

最后:本人也是刚开始学习 jQuery 源码,此文仅作为学习笔记和心得分享,关于Deferred,也还有很多不理解的地方,欢迎各位大佬指点

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容