jQuery deferred 使用方式的歪解

以下内容纯属自己在学习期间的心得体会记录,也希望对人有帮助,欢迎指出不足。

 

        jQuery的Deferred模块用来解决异步编程的回调函数注册问题。

        对于前端来说,最熟悉的异步操作自然是ajax了。我们来看看在JQuery1.5版本以前的写法是怎么样的。

    $.ajax({
    url: "ceshi.html",
    success:function(){
      alert("成功!");
    },
    error:function(){
      alert("出错!");
    }
  });


       这就注册了两个函数,异步网络请求执行成功和失败的回调函数。而这两个函数最终会在哪里执行呢?

      在jQuery代码里面,用原生JS来写的话,这两个函数的执行地点也很简单。简单示例如下(jQuery里面自然比这里写的要复杂):

    function ajax(setting){
         var xhr = newXMLHttpRequest();
         xhr.onreadystatechange= function(){
                   if(xhr.readyState=== 4){
                            if(xhr.status=== 200){
                                     setting.success(xhr.responseText);
                            }else{
                                     setting.error(xhr);
                            }
                   }
         };
         xhr.open("GET", setting.url,true);
         xhr.send(null);
    }


        可以看得出,注册的两个回调函数,他们最终会在满足某个条件的状况下选择一个被执行。在这两个注册函数上面,还有一层事件注册函数。所以呢,这俩注册的回调函数被执行的时间是不确定的,需要由事件何时发生来决定。

        但是,我们在这里可以这样子来理解这两个函数,我们在调用最外面的$.ajax()函数时,就会在将来的某个时刻,定会执行的代码中,放了一个鱼钩加饵进去(也就是被注册的俩回调函数,相当于一个钩子。),等着鱼来吃。这说起来就和钓鱼一样,鱼什么时候会上钩,就只有等待,等着事件发生,也就是等着鱼来上钩。

        只要鱼吃掉了鱼饵,也就是onreadystatechange这个事件被唤醒了,那么我们注册的函数肯定就会知道从而被执行。至于哪个函数会被执行,就要由onreadystatechange的事件函数来决定。

        这样子说来,异步编程就是放鱼钩等鱼来吃,鱼上钩了就会按既定程序来拉鱼竿的模式了。说专业话就是注册回调函数,等待将来某个时刻被执行。而对于异步编程来说,这样子放鱼钩的方式几乎是不会变的,但是注册回调函数的方式却是能变的。这就是deferred所做的改变。

我们来看看 jQuery 1.5版本后,采用deferred模块来注册回调函数的方式。

$.ajax("ceshi.html")
  .done(function(){ alert("成功") })
  .fail(function(){ alert("出错")});

        唯一的变化就是不再由一个函数$.ajax来传入所有的设置和回调函数。而是分为三步走。先传入请求的网页地址,再注册网络异步请求执行成功的回调函数,最后注册网络异步请求执行失败的回调函数。而成功和失败的回调函数注册是可以互换位置的,失败在前,成功在后也行,注册的顺序不限制。(到了这里就有点想吐槽的了,最后再说吐槽点。)

        这和以前的方式相比,大体来看差别不是很大,只不过将两个回调函数的注册交给了另外两个接口函数而已。不再是一步到位,而是三步到位。

        这有什么好处???

        我所知道的有三点好处。

        一、是可以让以前传入的一个统一的设置对象分隔开来,这种链式分步骤的写法让程序更容易阅读。

        二、是可以注册多个回调函数。就像下面这样

$.ajax("test.html")
  .done(function(){ alert("成功")} )
  .fail(function(){ alert("出错")} )
  .done(function(){ alert("第二次成功")} )
    .fail(function(){alert( “第二次出错”)});


       注册的多个回调函数会依注册顺序依次执行。

       三、是可以为多个异步操作注册一个统一的回调函数,像下面这样。

    

   $.when($.ajax("ceshi1.html"),$.ajax("ceshi2.html"))
      .done(function(){ alert("成功") })
     .fail(function(){alert("出错") });

        在两个异步操作都成功后,执行成功的注册函数。只要其中有一个异步执行失败,就会执行失败的注册函数。when是deferred模块的一种接口。

其他的好处只有求高手回答了。

        第一个好处其实并不是太大,而对于第二和第三来说,就甚为有意义了。以前老的写法也可以实现第二和第三种好处,但是,肯定没deferred的方式清晰明了和方便。

还有一点要注意,虽然都是以ajax来举例,但是deferred的应用对象可不只是AJAX。只要是异步的操作(比如HTML5中本地数据库的操作),需要注册回调函数,就都可以使用这种方式。

        那么,新问题来了,怎么使用deferred呢?对于AJAX的操作,jQuery已经帮我们封装好了使用方式,所以对于上面的例子有可能看起来不是太明白。

想明白使用方式,我们要回到钓鱼这个话题上来。在上面的代码示例中,有一个原生JS使用AJAX的例子。在那里面我们下了两个钩子,用来主动调用了两个注册函数,也就是setting.success(xhr.responseText)和setting.error(xhr);。这是对于用以前老的方式来注册回调函数而言,就直接执行注册的回调函数。

        而对于使用了deferred模块后,该怎么样来让注册的回调函数执行呢?也不复杂,增加了几个deferred的东西而已。如下所示。

    function ajax(url){
       // 新建一个deferred对象
       var def =$.Deferred();
       var xhr =new XMLHttpRequest();
       xhr.onreadystatechange= function(){
              if(xhr.readyState=== 4){
                     if(xhr.status=== 200){
                            // 改变deferred对象的状态为成功
                            def.resolve(xhr. responseText);
                     }else{
                            // 改变deferred对象的状态为失败
                            def.reject (xhr);
                     }
              }
       };
       xhr.open("GET",url , true);
       xhr.send(null);
       //返回promise对象
       return def.promise();
   }

        和上面原生JS的代码对比一下,会发现ajax函数首尾增加了两行代码,注册的回调函数执行的地方,更改了两行代码。这样子就把一个有异步操作的原生JS函数ajax,改成了用deferred模块来实现的函数。

        然后来解释下这四行代码的作用。清楚明白后就可以把任意的有异步操作的函数改成用deferred来实现。从而就可以使用最上面的deferred方式(用done、fail、 then、progress、always等函数来注册)来注册回调函数,也就拥有了上面所说的三点好处。而这四行代码可能是deferred最让人难理解的地方了。

       先来看看中间被更改的两行代码。

       这里没有直接调用注册的回调函数,也就是没有直接拉鱼竿。为什么啊?因为没有函数的引用让我们来调用啊,ajax这时候只传入了一个url参数,能调用到个屁啊。那我们调用的是什么啊?

       我们主动调用了Deferred对象的两个方法接口resolve和reject。翻译过来而言,和成功失败是差不多的道理。那也就很好理解了。这鱼钩和饵(注册的回调函数)在这里变了,不是我们主动投下去的,而是有一个代理帮我们投下去的(done、fail等接口注册回调函数),还提供帮助,给了我们拉鱼竿的权力(执行已注册的回调函数,也就是def.resolve()和def. reject ())。

        以前的老方式是两步走,注册回调函数,执行回调函数。现在用deferred也是如此,只不过变成了注册是用done、fail这个接口,而执行变成了用resolve和reject这些个接口实现。

       上面说的代理就是deferred对象了。这和订阅/发布者模式何其相似啊,简直是一模一样的。投饵就是订阅,拉鱼竿就是发布。订阅就是注册回调函数,发布就是执行已注册的回调函数。

        以前注册回调函数是直接传进去,现在呢,先不传,而是缓一步再传。注册就要用到代理deferred给我们提供的接口——done、fail、then、always和progress等。

        说是代理感觉也不太好理解。可以把deferred看成第三方专门提供帮助的人,是你的女仆如何?

        在这里,你有命令女仆做事的权力。你可以在看见鱼上钩后,命令女仆拉鱼竿(resolve)。或者发觉鱼跑了,让女仆换鱼饵(reject)。这个看见是在注册的事件函数里的,所以这个看见是不可测的异步操作。

        当调用了resolve命令,也就是这样def.resolve()后,注册的所有成功时执行的回调函数都会依次执行。而调用reject命令,也就是这样def. reject ()后,注册的所有失败时执行的回调函数都会依次执行。

        当然,同时也可以传递参数给所有回调函数,只需要在命令函数里加上参数即可。比如这样。def.resolve(‘嗨,各位大家好’)。那么done(function(a){alert(a)}),注册的函数即可从a参数接收到。最终弹出窗口显示“嗨,各位大家好”。传递的参数个数不限。

       deferred对象有三种执行状态----未完成,已完成和已失败。从未完成到已完成或已失败的状态间转换就靠这两个命令了。

       其实还有其他的命令(notify、notifyWith、resolveWith、rejectWith),但在这里暂时知道这两个就行了,其他的命令在理解deferred后,自己看文档就能看懂。

       中间两行代码说到底也是在执行注册的回调函数,只不过借了别人的嘴来命令它们执行而已。

       那么首行创建deferred对象的代码就好理解了。每次异步操作,都需要一个deferred来帮助我们。所以每个有异步操作的函数里,我们都要新建一个deferred。

       好了,现在我们来看看最下面的函数返回值——返回一个promise对象,从deferred身上生成的。这个promise有什么用哦?

       上面说到了,注册回调函数是缓了一步再注册的。但是在哪里去注册啊,注册给谁啊?这就是promise的作用。也就是用promise对象提供的接口去注册回调函数。所以改成deferred模式的ajax函数可以如下这样写了。

ajax(‘ceshi.html’).done().fail();

       ajax函数返回了promise对象,而promise对象提供了done、fail这些注册回调函数的接口方法。而done和fail这些接口在调用后又返回自身对象,所以就可以这样连缀的写下来,到达了缓一步注册的目的,也让异步看起来变成了同步。

        “异步变同步”这点对于Node而言简直就是大善!!让大量的回调看起来变成了同步执行。比如上面的例子,阅读代码时看起来就像是,先ajax去请求网页,然后成功就怎么样,失败又怎么怎么样的。

        但是像事件注册这样的写法,一般都是先注册,再调用。而deferred是先调用后,再来注册回调函数,所以有些人在这里会患迷糊。(所以也有个槽点了)

        疑问来了,为什么不直接把接口放在deferred身上来返回呢?这又是deferred,又是promise的,不嫌麻烦啊!而这样做当然是有原因的。

        看下面这个直接返回deferred对象的ajax函数,事实上deferred对象上是有注册回调函数的接口done、fail、then、always和progress等的。所以确实可以直接返回。那么来看看这样子返回有什么不好吧。

    function ajax(url){
       // 新建一个deferred对象
       var def =$.Deferred();
       var xhr =new XMLHttpRequest();
       xhr.onreadystatechange= function(){
              if(xhr.readyState=== 4){
                     if(xhr.status=== 200){
                           // 改变deferred对象的状态为成功
                            def.resolve(xhr.responseText);
                     }else{
                            //改变deferred对象的状态为失败
                            def.reject (xhr);
                     }
              }
       };
       xhr.open("GET", url , true);
       xhr.send(null);
       //返回deferred对象
       return def;
    }
    var def = ajax(‘ceshi.html’)
              .done(function(){alert(‘成功’)})
              .fail(function(){alert(‘失败’)});
    //注意这里!!我们在ajax外部来调用女仆定会听从的命令resolve
    def.resolve ();

        上面这段代码的执行结果是立即弹出窗口,内容是“成功”。为什么?因为我们把deferred的命令接口暴露出来了。我们在外面就可以命令这可怜的女仆做任何事。相当于执掌虎符的大将军把虎符主动送给别人,这军队自然归别人掌管了,就任人宰割。这样子自然非常不好。

        所以我们不能返回有命令接口的deferred对象,而是只有注册回调函数接口的promise对象。这样子别人怎么也命令不了你自己的女仆了。对外开放的只有放饵的权力,鱼上钩了后,拉鱼竿就轮不到别人来做,只有自己能做,这多好的事情啊。

 

        综上所述,如果自己有一个有异步操作的函数,要把它变为deferred模式就需要三步。

        第一步需要有一个deferred对象,一般通过$.Deferred()生成。

        第二步根据自己解决问题和业务的逻辑来确定调用哪个命令,一般是resolve ()和reject();

        第三步,通过deferred对象生成的promise作为函数返回值。一般这样子写,return def.promise();

       如此,我们便可以用deferred的方式来使用这个函数了。

       有哪几种使用方式呢?也就是哪几种注册回调函数的方式。

        通过done和fail接口是最常见的。还有then接口,他一次性接收两个注册函数,第一个参数是成功时的回调函数,第二个是失败后的回调函数。

ajax(‘ceshi.html’).then(function(){alert(‘成功’)},function(){alert(‘失败’)});

        注意then接口在jQuery1.8版本后又有了新的变化,可以用来过滤改变,命令接口传入的参数值。参考官方文档http://api.jquery.com/deferred.then/

        还有progress、always 、jQuery.when()以及pipe,使用方法下次再说吧。

        有异步操作的函数里通常需要自己生成一个deffered辅助对象,就相当于自己买个女娃娃当仆人了。

其实有一种方式可以不用自己生成,而是让$.Deferred方法从函数外面由参数传递进来。$.Deferred(ajax).done().fail()。如果采用这样的写法,说明ajax函数第一个参数就是一个deferred对象,直接用就行了,不用新建。但有个缺点,ajax函数这样子就会接收不到其他参数,而只有一个参数。更大的缺点是deferred对象从$.Deferred接口的返回值暴露出来了,所以不推荐这种做法。

        说到这里,反正我自己心里对于使用deferred的方式是大概理解到了。也希望上面这一大段乱七八糟的文字能对人有帮助。

        最后,来说说那个吐槽点。对于先执行再注册这种模式,一看就有点矛盾了。万一异步操作执行完毕后,注册却还没执行到,造成注册失败,那么不就做了无用功嘛!不过,在很大很大部分情况下,这种情况是很少很少发生的。因为从执行异步函数完毕到注册成功,只有短短那么几个链式写法的注册函数跳转而已。如果真遇到这情况,那么这异步操作也真没异步的必要了,直接同步不是更好?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值