zoukankan      html  css  js  c++  java
  • 关于Promise的一些个人理解jQuery的deferred

     


    一、什么是deferred对象?

    开发网站的过程中,我们经常遇到某些耗时很长的javascript操作。其中,既有异步的操作(比如ajax读取服务器数据),也有同步的操作(比如遍历一个大型数组),它们都不是立即能得到结果的。

    通常的做法是,为它们指定回调函数(callback)。即事先规定,一旦它们运行结束,应该调用哪些函数。

    但是,在回调函数方面,jQuery的功能非常弱。为了改变这一点,jQuery开发团队就设计了deferred对象

    简单说,deferred对象就是jQuery的回调函数解决方案。在英语中,defer的意思是"延迟",所以deferred对象的含义就是"延迟"到未来某个点再执行。

    它解决了如何处理耗时操作的问题,对那些操作提供了更好的控制,以及统一的编程接口。它的主要功能,可以归结为四点。下面我们通过示例代码,一步步来学习。

    来看下面的一段代码

    function step1(callback) {
        $.ajax({
            url : '/api/test',
            type : 'POST',
            data : {
                ...
            },
            success : function (res) {
                callback && callback();
            }
        })
    }

    当我们使用回调来解决实际中的问题时,很容易不知不觉中出现代码金字塔,嵌套层次就会越深,代码可读性就会越差。

    step1(function () {
        step2(function () {
            step3(function () {
                step4(function () {
                    step5();
                })
            })
        })
    })

    ES6 原生提供了 Promise 对象。

    所谓 Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。

    Promise 对象有以下两个特点。

    (1)对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和 Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。

    (2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

    有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

    Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

    基本的 api

    1. Promise.resolve()

    2. Promise.reject()

    3. Promise.prototype.then()

    4. Promise.prototype.catch()

    5. Promise.all() // 所有的完成

    所谓“承诺”

    Promise这个单词的意思是“承诺”,就是我们日常说的“我肯定帮你买早饭”、“你如果交了钱我肯定给你一杯咖啡”。

    在程序世界,举例可以说:“我承诺给你完成这些代码执行”。new一个Promise实例,就是JS引擎对你做了一个承诺。

    既然是承诺,就肯定有成功的时候有失败的时候,比如我帮你买早餐,结果今天煎饼果子没出摊,或者是我走到半路上,煎饼果子的塑料袋破裂,煎饼果子滑落到了地上,这就是失败。就连“我们承诺绝不首先动用核武器”都有坚持不下去的时候,所以只要是承诺就有成功和失败,只不过是概率问题。

    到程序世界,一个承诺也会有三种状态,就是“未决的”、“成功的”、“失败的”三种状态。也就是pendding/resolved/rejected三种状态。


    Promise构造函数的超能力

    Promises写法的本质就是把异步写法撸成同步写法。要做这么酷炫这么变态的事情,当然需要Promise构造函数有超能力,它的超能力就是传入Promise构造函数的函数参数会第一优先执行,无论这个函数多么的繁复,有多少层回调,有多少秒的计数器,统统都会最优先执行,也就是说,我们只要new了一个Promise(),那么Promise构造函数的函数参数就是最高优先级执行,一直到new出一个promise对象实例,后面的.then()代码才会执行。链条上的每一个.then都会等前面的promise有了结果才会执行,Promise构造函数的这个超能力是Promises系统的威力之源。(当然,这里说的执行优先级,是在理想环境下,所谓理想环境也就是全部执行代码只由new Promise()和它的一系列.then()方法组成。如果方法链之外还有其他代码,那么整体代码执行的先后顺序就复杂化了,下文有介绍。然而,Promise加它的then方法链已经提供了梳理代码执行顺序的整套方案,如果在方法链之外还写代码的话属于画蛇添足、自找麻烦,是不科学的写法,应该避免这么做。)

    ajax操作的链式写法

     $.ajax("test.html")
    
      .done(function(){ alert("哈哈,成功了!"); })
    
      .fail(function(){ alert("出错啦!"); });

    deferred对象的最大优点,就是它把这一套回调函数接口,从ajax操作扩展到了所有操作。也就是说,任何一个操作----不管是ajax操作还是本地操作,也不管是异步操作还是同步操作----都可以使用deferred对象的各种方法,指定回调函数。

    我们来看一个具体的例子。假定有一个很耗时的操作wait:

    var wait = function(){
    
        var tasks = function(){
    
          alert("执行完毕!");
    
        };
    
        setTimeout(tasks,5000);
    
      };

    但是,这样写的话,done()方法会立即执行,起不到回调函数的作用。原因在于$.when()的参数只能是deferred对象,所以必须对wait()进行改写:

    var dtd = $.Deferred(); // 新建一个deferred对象
    
      var wait = function(dtd){
    
        var tasks = function(){
    
          alert("执行完毕!");
    
          dtd.resolve(); // 改变deferred对象的执行状态
    
        };
    
        setTimeout(tasks,5000);
    
        return dtd;
    
      };

    现在,wait()函数返回的是deferred对象,这就可以加上链式操作了。

     $.when(wait(dtd))
    
      .done(function(){ alert("哈哈,成功了!"); })
    
      .fail(function(){ alert("出错啦!"); });

    wait()函数运行完,就会自动运行done()方法指定的回调函数。

    deferred.resolve()方法和deferred.reject()方法

    要说清楚这个问题,就要引入一个新概念"执行状态"。jQuery规定,deferred对象有三种执行状态----未完成,已完成和已失败。如果执行状态是"已完成"(resolved),deferred对象立刻调用done()方法指定的回调函数;如果执行状态是"已失败",调用fail()方法指定的回调函数;如果执行状态是"未完成",则继续等待,或者调用progress()方法指定的回调函数(jQuery1.7版本添加)。

    前面部分的ajax操作时,deferred对象会根据返回结果,自动改变自身的执行状态;但是,在wait()函数中,这个执行状态必须由程序员手动指定。dtd.resolve()的意思是,将dtd对象的执行状态从"未完成"改为"已完成",从而触发done()方法。

    类似的,还存在一个deferred.reject()方法,作用是将dtd对象的执行状态从"未完成"改为"已失败",从而触发fail()方法。

    var dtd = $.Deferred(); // 新建一个Deferred对象
    
      var wait = function(dtd){
    
        var tasks = function(){
    
          alert("执行完毕!");
    
          dtd.reject(); // 改变Deferred对象的执行状态
    
        };
    
        setTimeout(tasks,5000);
    
        return dtd;
    
      };
    
      $.when(wait(dtd))
    
      .done(function(){ alert("哈哈,成功了!"); })
    
      .fail(function(){ alert("出错啦!"); });

    deferred.promise()方法

    上面这种写法,还是有问题。那就是dtd是一个全局对象,所以它的执行状态可以从外部改变。

    请看下面的代码:

    var dtd = $.Deferred(); // 新建一个Deferred对象
    
      var wait = function(dtd){
    
        var tasks = function(){
    
          alert("执行完毕!");
    
          dtd.resolve(); // 改变Deferred对象的执行状态
    
        };
    
        setTimeout(tasks,5000);
    
        return dtd;
    
      };
    
      $.when(wait(dtd))
    
      .done(function(){ alert("哈哈,成功了!"); })
    
      .fail(function(){ alert("出错啦!"); });
    
      dtd.resolve();

    我在代码的尾部加了一行dtd.resolve(),这就改变了dtd对象的执行状态,因此导致done()方法立刻执行,跳出"哈哈,成功了!"的提示框,等5秒之后再跳出"执行完毕!"的提示框。

    为了避免这种情况,jQuery提供了deferred.promise()方法。它的作用是,在原来的deferred对象上返回另一个deferred对象,后者只开放与改变执行状态无关的方法(比如done()方法和fail()方法),屏蔽与改变执行状态有关的方法(比如resolve()方法和reject()方法),从而使得执行状态不能被改变。

    请看下面的代码:

    var dtd = $.Deferred(); // 新建一个Deferred对象
    
      var wait = function(dtd){
    
        var tasks = function(){
    
          alert("执行完毕!");
    
          dtd.resolve(); // 改变Deferred对象的执行状态
    
        };
    
        setTimeout(tasks,5000);
    
        return dtd.promise(); // 返回promise对象
    
      };
    
      var d = wait(dtd); // 新建一个d对象,改为对这个对象进行操作
    
      $.when(d)
    
      .done(function(){ alert("哈哈,成功了!"); })
    
      .fail(function(){ alert("出错啦!"); });
    
      d.resolve(); // 此时,这个语句是无效的

    在上面的这段代码中,wait()函数返回的是promise对象。然后,我们把回调函数绑定在这个对象上面,而不是原来的deferred对象上面。这样的好处是,无法改变这个对象的执行状态,要想改变执行状态,只能操作原来的deferred对象。

    不过,更好的写法是将dtd对象变成wait()函数的内部对象。

     var wait = function(dtd){
    
        var dtd = $.Deferred(); //在函数内部,新建一个Deferred对象
    
        var tasks = function(){
    
          alert("执行完毕!");
    
          dtd.resolve(); // 改变Deferred对象的执行状态
    
        };
    
        setTimeout(tasks,5000);
    
        return dtd.promise(); // 返回promise对象
    
      };
    
      $.when(wait())
    
      .done(function(){ alert("哈哈,成功了!"); })
    
      .fail(function(){ alert("出错啦!"); });

    普通操作的回调函数接口(中)

    另一种防止执行状态被外部改变的方法,是使用deferred对象的建构函数$.Deferred()。

    这时,wait函数还是保持不变,我们直接把它传入$.Deferred():

     $.Deferred(wait)
    
      .done(function(){ alert("哈哈,成功了!"); })
    
      .fail(function(){ alert("出错啦!"); });

    除了上面两种方法以外,我们还可以直接在wait对象上部署deferred接口。

    var dtd = $.Deferred(); // 生成Deferred对象
    
      var wait = function(dtd){
    
        var tasks = function(){
    
          alert("执行完毕!");
    
          dtd.resolve(); // 改变Deferred对象的执行状态
    
        };
    
        setTimeout(tasks,5000);
    
      };
    
      dtd.promise(wait);
    
      wait.done(function(){ alert("哈哈,成功了!"); })
    
      .fail(function(){ alert("出错啦!"); });
    
      wait(dtd);

    这里的关键是dtd.promise(wait)这一行,它的作用就是在wait对象上部署Deferred接口。正是因为有了这一行,后面才能直接在wait上面调用done()和fail()。

    小结:deferred对象的方法

    前面已经讲到了deferred对象的多种方法,下面做一个总结:

      (1) $.Deferred() 生成一个deferred对象。

      (2) deferred.done() 指定操作成功时的回调函数

      (3) deferred.fail() 指定操作失败时的回调函数

      (4) deferred.promise() 没有参数时,返回一个新的deferred对象,该对象的运行状态无法被改变;接受参数时,作用为在参数对象上部署deferred接口。

      (5) deferred.resolve() 手动改变deferred对象的运行状态为"已完成",从而立即触发done()方法。

      (6)deferred.reject() 这个方法与deferred.resolve()正好相反,调用后将deferred对象的运行状态变为"已失败",从而立即触发fail()方法。

      (7) $.when() 为多个操作指定回调函数。

    除了这些方法以外,deferred对象还有二个重要方法,上面的教程中没有涉及到。

      (8)deferred.then()

    有时为了省事,可以把done()和fail()合在一起写,这就是then()方法。

     $.when($.ajax( "/main.php" ))
    
      .then(successFunc, failureFunc );

    如果then()有两个参数,那么第一个参数是done()方法的回调函数,第二个参数是fail()方法的回调方法。如果then()只有一个参数,那么等同于done()。

    deferred.always()

    这个方法也是用来指定回调函数的,它的作用是,不管调用的是deferred.resolve()还是deferred.reject(),最后总是执行

     $.ajax( "test.html" )
    
      .always( function() { alert("已执行!");} );

    本文转载:http://www.ruanyifeng.com/blog/2011/08/a_detailed_explanation_of_jquery_deferred_object.html

    如果这篇文章对您有帮助,您可以打赏我

    技术交流QQ群:15129679

  • 相关阅读:
    MyBatis常见面试题以及解读
    如何防止sql注入攻击
    宝塔Linux面板基础命令
    Centos7配置静态ip
    宝塔Linux面板安装
    idea中安装阿里巴巴的代码规范插件
    idea中快速将类中的属性转为Json字符串的插件
    创建线程的四种方式
    sleep()方法与wait()方法的区别
    解决线程安全的几种方式
  • 原文地址:https://www.cnblogs.com/yeminglong/p/9504814.html
Copyright © 2011-2022 走看看