破解jQuery Deferred()异步执行的神秘世界

本文解析jQuery.Deferred()源代码,学习异步执行实现原理和编程技术。

测试代码:
var f1=function(){
  var deferred = $.Deferred();
  deferred.resolve('f1 resolved');
  return deferred.promise();
}
f1().then(function(data){
  //$.when().then(function(){
    console.log(data);
    var deferred = $.Deferred();
    setTimeout(function(){
      deferred.resolve('then resolved');
    },2000);
    return deferred.promise();
}).done(function(data){ // 也可以写.then(f1,f2,f3)
  console.log(data);
}).fail(function(){
  console.log('fail');
}).progress(function(){
  console.log('progress');
});

 

 

jQuery Deferred()源代码分为两个嵌入到jquery对象的函数块:

jQuery.Callbacks = function( options ) {
  定义promise/deferred对象的内部操作方法以及内部数据...

jQuery.extend({
  Deferred: function( func ) {
    定义promise/deferred对象以及暴露的方法比如resolve/then...

 

Deferred()代码:

Deferred: function( func ) {

  var tuples = [
    // action, add listener, listener list, final state
    [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], // jQuery.Callbacks调用结果是返回一个self{}对象,含内部处理函数
    [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
    [ "notify", "progress", jQuery.Callbacks("memory") ]
  ],

  state = "pending",  // 这个就是当前Deferred实例的状态

  promise = {}; //返回的实例(子实例)

  deferred = {}; //当前实例(父实例,含子实例)


  jQuery.each( tuples, function( i, tuple ) {
    var list = tuple[ 2 ], // tuples是调Callbacks获取self对象,因此list引用self对象
    // deferred[ resolve | reject | notify ]
    deferred[ tuple[0] ] = function() { // 循环三次分别构造deferred.resolve/reject/notify函数代码
      deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); //调deferred.resolveWith,构造参数,resolve函数是deferrred
                    的内置函数,this就是deferred,arguments就是调resolve时的入口参数,比如resolve(data)。此句即为:
                      deferred.resolveWith(promise,arguments);
                    传递的作用域object是promise而不是deferred。
                    resolveWith就是fireWith,因此就是调fireWith(promise.arguments),fireWith函数代码如下:       

                    // Call all callbacks with the given context and arguments
                    fireWith: function( context, args ) { // context是promise,args是调resolve传递的参数
                      fire( args );  // fire函数在jQuery.Callbacks函数中定义,下面是Callbacks函数结构
                        

                        //jQuery.Callbacks相当于定义一个类,从外部调self里面的public方法,public方法再调private方法
                        jQuery.Callbacks = function( options ) {
                          list = [],  // 执行then时把内部callback存储在list[],内部callback会调用应用callback

                          // Fire callbacks
                          fire = function( data ) {
                            list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) // 执行list[]里面的内部callback
                          }

                          

                          // Actual Callbacks object
                          self = {
                            // Call all callbacks with the given context and arguments
                            fireWith: function( context, args ) {
                              fire( args );
                            }
                          }

                          return self; // Deferred()会调用这个函数,引用self,因此Callbacks函数执行完之后self还存在,list[]也是。  

    deferred[ tuple[0] + "With" ] = list.fireWith; // deferred.resolveWith

    //因此resolve方法其实就是调用 resolve -> resoveWith = list.fireWith (引用self) -> fire (执行list[]里面的callback),代码设计非常复杂深奥,它没有用new实例方法,而是利用了js对象引用机制,函数定义一个对象,在其它位置引用这个对象,这个对象就一直存在,直到引用它的函数不存在为止,其实用new实例方法可以简化设计不用这么深奥。

});

 

下面来看then代码,Deferrred最复杂深奥的代码在then代码,体现了作者高超的编程技术,令人叹为观止:

then: function( /* fnDone, fnFail, fnProgress */ ) {

  var fns = arguments;
  return jQuery.Deferred(function( newDefer ) {  // then是Deferred实例里面的方法,注意这是递归再次调用Deferred创建子实例,当前有一个deferred,又递归再次执行函数新建一个deferred,至此就有两个deferred实例,也就是父子实例,子实例返回以便连写.then(callback),对于下一个.then来说就是父实例,执行下一个.then时又递归执行Deferred函数新建一个子实例返回,以此类推,连写嵌套多少层都一样。

    jQuery.each( tuples, function( i, tuple ) {
      var action = tuple[ 0 ],
      fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
      // deferred[ done | fail | progress ] for forwarding actions to newDefer
      deferred[ tuple[1] ](function() { // 循环三次就是执行:
        //deferrred.done(callback); // done是注册callback,意思是完成以后执行callback,前一个promise对象resolve时会执行callback。
        //deferrred.reject(callback); // 类似
        //deferrred.notify(callback); // 类似
        //每次都用相同的callback。

        //这是注册内部callback(匿名函数),要注册到当前deferred实例,deferred是then所在的实例,then递归调用Deferred新产生的实例也是deferred,但它是传递给回调,因此回调的newDefer是新建的子实例。奥妙就在于回调匿名函数function(newDefer){},在递归调用Deferred新建一套实例时用回调函数代码引用当前已经存在的实例!

        //当resolve时会执行内部callback匿名函数,会执行应用callback(也就是then里面的callback),然后再resolve当前promise对象,执行下一个then里面的callback。

        //为什么不直接执行应用callback是因为要处理解决当前promise对象,要判断应用callback是否返回promise对象,所以写了一段内部callback。

        var returned = fn && fn.apply( this, arguments ); //fn是从then参数获取的应用callback,执行then里面的应用callback。
        if ( returned && jQuery.isFunction( returned.promise ) ) { //下面代码的目的就是要 resolve当前then返回的promise对象从而执行下一个then里面的callback
          returned.promise()
            .done( newDefer.resolve ) //如果执行then里面的callback返回promise对象,就注册一个callback,否则就直接执行resolveWith
            .fail( newDefer.reject )
            .progress( newDefer.notify );
        } else {
          newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
          // 第一次循环时执行newDefer.resolveWith(promise,[returned]),就是直接去执行下一个then里面的callback并且传递执行应用callback返回的数据。

          //其它循环执行rejectWith/NotifyWith,类似。

        }
      });
      fns = null;
    }).promise(); //用promise()方法获取promise对象实例返回,promise对象实例只有部分方法,而deferred对象实例有所有的方法
  },

 

可能有点乱有点晕,下面小结一下:

执行then时把内部callback注册存储在list[],当resolve当前promise对象时,就是去list[]找callback执行,内部callback会执行应用callback(then里面的callback),再根据返回值resolve当前promise对象,从而执行下一个then里面的callback。

jQuery Derferred代码比es6-promise复杂,es6-promise用父子实例,then返回的实例是子实例,then把callback存储到父实例,这样当resolve父实例时就从父实例找callback执行即可,比较简单。但jQuery defer设计了一个Callbacks函数,相当于定义了一个类,有public/private方法,有self{}和list[]。执行then时构造一个内部callback存储在当前实例(父实例)的list[],当resolve当前实例时就去list[]找内部callback执行,内部callback会执行应用callback(then里面的callback),再resolve返回的子实例去list[]执行内部callback,这就是执行下一个then()里面的callback。每一层then都要重新递归执行jQuery.Deferred(func)新建一套子实例返回,下一个then就是新建子实例的内置方法,当执行下一个then时,then所在的实例就是父实例,递归执行Deferred新建实例就是子实例,如果连写多个.then则如此层层嵌套递归。递归是很复杂的,它是把当前程序代码又执行一遍,又产生一套实例,两套代码和实例完全一样,嵌套递归多少层重复执行多少次都不出错,如何区分处理父子实例需要高超的对象编程技术。

 所以jQuery Deferred程序和前端框架编译程序一样都是递归程序,重复执行多次就产生多个实例,就递归多个子节点,每一次递归执行时的位置和环境有所变化。

所以通用软件要解决复杂编程技术问题,而相比之下应用软件一般都不涉及复杂编程技术,是两个相当不同的编程世界。

 

最后一个问题就是如何修改实例的state状态:

state是在Deferred实例中定义的:
  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",

有一个内部callback负责修改state状态,其代码和注册位置:
    jQuery.each( tuples, function( i, tuple ) {
      stateString = tuple[ 3 ];
      // Handle state
      if ( stateString ) { // resolved或rejected
        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 ); //如果resolved就disable rejected,如果rejected就disable resolved
    }

执行内部callback时传递promise为作用域:
list[ firingIndex ].apply( data[ 0 ], data[ 1 ] // data[0]=promise data[1]=resolve参数

那么执行function(){state = stateString;} 时就是修改实例中的state,传递作用域为promise有何意义?执行这个方法时是在Callbacks程序空间,如何能访问到deferred实例中的state呢?

这个函数是在Deferred程序空间构造的,因此可以访问state,很简单,虽然把它放到list[]中,再调用执行,但函数是对象,就是引用,这个函数对象是在Deferred
程序空间定义的,就能访问Deferred程序空间的变量数据。

jQuery Deferred没有采用new实例方法,而是利用js对象引用机制,给阅读程序带来很大的迷惑,函数封装绕来绕去就是访问函数定义的一个对象,按程序机制,函数执行完就清除不存在了,包括局部数据都清除不存在了,但它还能在之后访问函数定义的对象。

promise机制其实类似trigger/on机制,都是先注册保存callback,再事件触发执行callback,只是它可以连写,形式上非常直观简洁,能连写是因为它返回promise对象,返回promise对象可不简单,就要涉及递归编程技术,trigger/on之类的程序不是递归程序,所以不能连写,能连写必然是递归程序。而callback的注册保存方法各种软件各不相同,用new实例方法最简明,没有迷惑,不用new实例方法就更复杂,很迷惑,因为那么多独立定义的函数和变量,在不同的程序空间,有些看似没关系,你如何能保证随时随地从某一个函数能访问到某一个变量?

还有一些处理细节也挺复杂的,本文不再分析。。。

bye

 

posted @ 2018-05-30 11:24  pzhu1  阅读(618)  评论(0编辑  收藏  举报