七、jQuery源码分析——Deferred:延迟对象:对异步的统一管理

Deferred延迟主要实现对异步的统一管理。本片文章主要较介绍Deferred对象的使用。

一、简化结构

jQuery.extend({
    Deferred: function(){},
    when: function(){}
})

// 对外提供的接口是
$.Deferred()
$.when()

二、用法

1、Deferred对象的使用方式类似于Callbacks对象的使用,因为Deferred封装和调用Callbacks对象来时先功能的。首先看下面的一组代码:


image.png

这两段代码运行的结果都是一样的,经过1秒后,先弹出111, 再弹出222.
可以简单的认为 resolve方法就是Callbacks对象中的fire方法。done方法就是Callbacks对象中的add方法。
2、其实,Deferred对象有多种状态,也就有相对应的多种回调函数。具体如下代码所示:


image.png

通过这组代码,我们可以知道,notify、resolve和reject都调用的Callbacks对象的add方法,progress、done、fail都调用的是Callbacks对象的fire方法。
3、可以使用延迟对象Deferred对jQuery.ajax()进行改造。
//传统的ajax的写法
$.ajax({
  url:'xxx.php',
  type: 'get',
  success:function(){
      alert('success')
  },
  error:function(){
    alert('fail')
  }
})
// 改造后的写法
$.ajax('xxx.php').done(function(){ alert('成功!') }).fail({ alert('失败') })

二、源码分析

1、定义方法调用元组

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") ]
]

1.1、首先,定义了一个数组tuples,保存状态和方法以及他们之间的对照关系。数组中每一条数据的参数的意思反别为:促成某种状态的方法(action)、执行方法(listener)、Callbacks回调对象(list)和 状态(state)。

1.2、至于为什么数组的前两项的Callbacks对象传入的参数为“once memory”,而第三项传入的参数为"memory",请看下面的示例代码:

这段代码运行的结果为 111,success,111,111,...,虽然使用了setInterval间歇调用函数,但是当调用了dfd.resolve()方法后,延迟对象dfd的状态已经被锁定为成功,所以第二次将不会在调用,这就要传入上面问题中传入“once”的缘由,保证只调用一次。当然调用dfd.reject() 和dfd.fail()方法也是一样的。

var dfd = $.Deferred()

setInterval(function(){
     alert(111)
    dfd.resolve()
},1000)
dfd.done(function(){
     alert('success')
})
var dfd = $.Deferred()

setInterval(function(){
     alert(111)
    dfd.reject()
},1000)
dfd.fail(function(){
     alert('success')
})

但是使用dfd.notify()和progress()方法方法时,会一直弹出\color{red}{111和"运行中"red},这当然是Callbacks对象中没有传递参数“once”的结果,示例代码如下:

var dfd = $.Deferred()

setInterval(function(){
      alert(111)
      dfd.notify()
},1000)
           
dfd.progress(function(){
     alert('运行中')
})

3、Callbacks中传入“memory”这个参数,可以实现”记忆功能“,因为当传递了参数memory,那么自第一次调用fire()方法后,下次调用add方法就会自动调用fireWith方法,对使用add()方法添加进去的回调函数都会实现调用。具体请看下面示例:

var cb = $.Callbacks('memory')
cb.add(function(){
      alert('aaa')
})

setTimeout(function(){
     cb.fire()
},1000)

$('input').click(function(){
     cb.add(function(){
     alert('bbb')
   })
})
var dfd = $.Deferred()

setTimeout(function(){
      alert(111)
       dfd.resolve()
},1000)

$('input').click(function(){
     dfd.done(function(){
           alert('bbb')
     })
 })

2、对应状态和方法

下面截取的代码片段中,会把\color{red}{回调方法}添加到promise对象上,
promise[ done | fail | progress ] = list.add,
\color{red}{状态方法}添加到deferred对象上,
deferred[ resolve | reject | notify ]

// line 3095
jQuery.each( tuples, function( i, tuple ) {
   // jQuery.Callbacks('once memory')  
    var list = tuple[ 2 ],    
  
    // promise[ done | fail | progress ] = list.add
    // 这段代码表示promise.done,  promise.fail,  promise.progress调用的都是Callbacks对象实例的add()方法
    promise[ tuple[1] ] = list.add;   
 
  // deferred[ resolve | reject | notify ]
    deferred[ tuple[0] ] = function() {
    deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
        return this;
    };
// 这段代码表示deferred[resolve|reject|notify] 调用的都是Callbacks对象实例的fireWith() 方法
  deferred[ tuple[0] + "With" ] = list.fireWith;

3、简化promise函数的结构

3.1 3053-3088行代码中,定义了promise对象,可以将其代码简化建构如下所示:


image.png

用色标注的方法是前面each遍历之后的为promise和deferred两个对象新增的方法。
3.2 然后通过 promise.promise( deferred )将promise对象的上方法全部拷贝到deferred对象上面。我们先看一下promise.promise这个方法:

promise: function( obj ) {
    return obj != null ? jQuery.extend( obj, promise ) : promise;
}

此处要主要,当不给promise传递参数,那么执行这个方法就会返回promise对象。具体的示例,请看下面代码:

function aaa(){
      var dfd = $.Deferred()
      setTimeout(function(){
             dfd.resolve()
       }, 1000)
      return dfd
 }

var newDerred = aaa()

newDerred.done(function(){
      alert('success')
}).fail(function(){
     alert('fail')
})
newDerred.reject()

运行上面的代码,因为有1000ms的延迟,所以可以在外部通过newDerred.reject()来改变Deferred的状态,所以会返回“fail”。如果将aaa函数中的return dfd换成 return dfd.promise(),那么就不会被外边的newDerred.reject()改变状态了。因为在调用dfd.promise()方法没有传参,所以会返回promise对象,而promise对象中并没有resolve,reject,notify等状态方法,所以代码除了运行正确还会有报错信息,Uncaught TypeError:newDerred.reject is not a function

4、promise各方法的使用

下面我们来看一下promise个方法的使用。

4.1 state() 获取Deferred对象的状态

var dfd = $.Deferred()
    alert(dfd.state())
setTimeout(function(){
    dfd.resolve()
     alert(dfd.state())
},1000)

dfd.done(function(){
     alert('success')
 }).fail(function(){
     alert('fail')
 })

上面的代码会首先弹出pending,然后1000ms后紧接着弹出success和resolved,关于Dererred对象状态更改的源码如下:

jQuery.each( tuples, function( i, tuple ) {
...
var stateString = tuple[ 3 ]

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 );
}

这段源码表明,在遍历tuples时,通过add方法将修改状态的回调函数添加进各自的Callbacks对象中,同时当状态确定之后(从penging转化为resolve,)

4.2、always() 不管成功还是失败,都会调用这个方法添加的回调

var dfd = $.Deferred()

setTimeout(function(){
       dfd.resolve()
 },1000)

dfd.done(function(){
     alert('success')
}).fail(function(){
    alert('fail')
}).always(function(){
    alert('always')
})

上面的代码执行后会先后弹出“success” 、“always”,如果使用dfd.reject()则相应的会先后弹出“fail”、“always”。其源码如下:

always: function() {
    deferred.done( arguments ).fail( arguments );
    return this;
}

这段源码表明,如果不执行正确的回调方法done、就执行错误的回调方法fail。总之,不会调用resolve()还是调用reject(),这个always()方法都可以起作用。

4.3、then(fnDone, fnFail, fnProgress)

除了上面的链式的调用,还可以以then的方式进行调用,具体如下所示:

var dfd = $.Deferred()

setTimeout(function(){
    dfd.resolve()
},1000)

dfd.then(function(){
   alert('success')
},function(){
    alert('fail')
},function(){
    alert('进行中...')
})

源码如下:

then: function( /* fnDone, fnFail, fnProgress */ ) {
    var fns = arguments;
    return jQuery.Deferred(function( newDefer ) {
        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() {
                    var returned = fn && fn.apply( this, arguments );
                    if ( returned && jQuery.isFunction( returned.promise ) ) {
                        returned.promise()
                            .done( newDefer.resolve )
                            .fail( newDefer.reject )
                            .progress( newDefer.notify );
                    } else {
                        newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
                    }
                });
        });
        fns = null;
    }).promise();
}

这段源码最核心的是下面的代码,也是我所不能理解的。

deferred[ tuple[1] ](function() {
         var returned = fn && fn.apply( this, arguments );
         if ( returned && jQuery.isFunction( returned.promise ) ) {
               returned.promise()
                      .done( newDefer.resolve )
                      .fail( newDefer.reject )
                       .progress( newDefer.notify );
            } else {
                   newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
           }
});

这个if循环是pipe()方法的代码。对于then()方法,我们主要关心的是var returned = fn && fn.apply( this, arguments )这段代码。按照前面的分析,deferred[ tuple[1] ]应该是deferred['done|fail|progress'],也就是执行Callbacks对象中的add()操作,但是 fn.apply( this, arguments )中this 和argements的传值我并不是很清楚。下面是两段示例代码:

var dfd = $.Deferred()

setTimeout(function(){
       dfd.resolve('aaa')
 },1000)

 dfd.then(function(){
       alert(arguments[0])      //aaa
},function(){
       alert('fail')
},function(){
       alert('进行中...')
})

按照这样上面的arguments对象是应该是dfd.resolve()这个地方传递进去的‘aaa',然后在后面的then()方法调用中被接收。
至于pipe方法,请看下面的示例:

var dfd = $.Deferred()

setTimeout(function(){
     dfd.resolve('aaa')
},1000)

var newDeferred = dfd.pipe(function(){
     return arguments[0] + '妙味'
})

newDeferred.done(function(){
    alert(arguments[0])        // aaa妙味
})

通过使用pipe管道方法,我们会得到一个新Deferred对象,并对其中的参数进行加工,这个尚且不能很好的理解。

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

推荐阅读更多精彩内容

  • 前言 本文旨在简单讲解一下javascript中的Promise对象的概念,特性与简单的使用方法。并在文末会附上一...
    _暮雨清秋_阅读 2,210评论 0 3
  • 源代码 deffered的使用说明() ajax的使用 运行代码示例在上面的代码中,$.ajax()接受一个对象参...
    YAMI_1d00阅读 414评论 0 0
  • 一、什么是deferred对象? 开发网站的过程中,我们经常遇到某些耗时很长的javascript操作。其中,既有...
    你为什么无理取闹阅读 402评论 0 4
  • 抽象来说,deferreds 可以理解为表示需要长时间才能完成的耗时操作的一种方式,相比于阻塞式函数它们是异步的,...
    北方蜘蛛阅读 1,564评论 1 5
  • 活在当下,爱在今天。 早上上班把平板和遥控器都背走了。 走之前喊小人起床啦!八点她开始给我打电话,要平板,我要求先...
    我爱家佳阅读 177评论 0 0