JavaScript 异步回调 / $.Deferred

JQuery.Defferred()是基于Promise/A规范,因为JQuery本身的设计风格,在之前的版本并没有完全遵照Promise/A规范,在JQuery3.0中实现了完全的Promise实现,并且在$.ajax中移除了success、error、complete方法,改为对应的done、fail、always方法

需要注意的是在低于1.5.0版本的JQuery中,ajax返回的是XHR对象,而在高于1.5.0的版本中返回的则是deferred对象

在JQuery1.5、1.6版本中Deffered对象耦合比较严重,推荐使用1.8以上版本

deferred对象可以用来更加方便的操作异步函数,正常情况下异步函数执行的时候是没有先后顺序的,可能我们有B函数需要依赖A函数中的某一个参数,但是B函数却会在A函数之前先执行完毕了,这种情况在NodeJS,gulp等都是非常常见的,而且更糟糕的是如果需要的某一个数据是使用AJAX来获取的,我们再外部调用数据很有可能会报错,因为AJAX是异步的

 var data;
 $.get('api/data', function(resp) {
         data = resp.data;
     });
doSomethingFancyWithData(data);

可能我们会想到将Ajax设置为同步来解决问题,但这毫无疑问会带来阻塞,更加正确的做法是为它指定回掉函数,也就是事先规定,一旦函数运行结束,应该调用那些函数,简单理解的话deferred对象就是JQuery的回调函数解决方案,deferred对象正像它的英文名含义一样,就是延迟到未来某个点再执行,而这个点就是异步函数执行完毕的时间

我们首先在代码中打印一下$.Deferredd对象

//Object
//    always:ƒ()
//    done:ƒ()
//    fail:ƒ()
//    notify:ƒ()
//    notifyWith:ƒ(context, args)
//    pipe:ƒ(/* fnDone, fnFail, fnProgress */)
//    progress:ƒ()
//    promise:ƒ(obj)
//    reject:ƒ()
//    rejectWith:ƒ(context, args)
//    resolve:ƒ()
//    resolveWith:ƒ(context, args)
//    state:ƒ()
//    then:ƒ(/* fnDone, fnFail, fnProgress */)
//    __proto__:Object

接下来我们开始看一下他们的大概用法

在之前我们使用ajax传统写法是

 $.ajax({
    url: "test.html",
    success: function(){
        ...
    },
    error:function(){
        ...
    }
  });

而在1.5.0之后ajax返回的是一个deferred对象,我们就可以使用deferred的写法来写

$.ajax("test.html")
  .done(function(){...})
  .fail(function(){...})
  .done(function(){...});

在JQuery中的Deferred对象上有一个pipe方法,该方法可以在执行done方法前对返回数据进行处理,在处理完成后可以通过renturn返回给done函数作为参数

 $.get("url_1")
       .pipe(res=>{
           console.log(res); //{data: Array(12), status: "success"}
           return res.data[0];
       })
       .done(res=> {
           console.log(res); //{id: null, name: "普工", code: "1", typeid: null, sortOrder: null, …}
       })
       .pipe(res=>{
           return res.name;
       })
       .done(res=> {
           console.log(res); //普工
       })

可以看到,done方法相当于success方法,fail相当于error方法,采用链式写法可以极大的提高代码的可读性,而且允许我们自由添加多个回调函数,添加的回调函数将会按照添加顺序执行,而且允许我们为多个事件指定一个回调函数,这是传统写法做不到的

 $.when(
        $.ajax({url: "./02.txt"}),
        $.ajax({url: "./03.txt"})
    )
        .always(function () {
            console.log("hello");
        })
        .done(function (x, y) { //
            console.log(x);
            console.log(y);
            //x,y都是数组,分别对应第一个和第二个ajax请求的返回值
        })
        .fail(function (e) {
            throw new Error("请检查路径");
        })

$.when()是jQuery提供的一个新方法,该方法会在两个操作都成功的情况下运行done指定的回调函数,如果有一个失败或都失败了就执行fail指定的回调函数,不论失败或成功都会执行always方法,需要注意一点,如果使用when方法,那么需要传入的参数必须都是deferred对象,否则的话起不到回调函数的作用

var wait = function(){
    var tasks = function(){
     alert("执行完毕")
    };
    setTimeout(tasks,2000);
  };
$.when(wait())
    .done(function(){ alert("成功")})
    .fail(function(){ alert("失败")});
//done中的方法会立即执行,因为wait不是deferred对象,$.when无效 

那么我们接下来看一下如何将一个普通函数变成一个deferred对象

在这之前,我们先了解一个概念,执行状态,JQuery中规定,deferred对象有三种执行状态:

正在执行、已完成和已失败

如果执行状态是已完成,那么deferred对象立刻调用done()方法指定的回调函数;如果是已失败,调用fail()中的回调函数,如果执行状态是未完成,则继续等待,或者定义了progress(),就调用该方法中指定的回调函数,在之前使用ajax时,JQuery会根据返回结果,自动改变自身的执行状态,但是在其它不是deferred对象的函数中需要我们手动指定执行状态,这个时候就需要我们用到$.deferred上的方法

<script>
    var def = $.Deferred(); //定义一个deferred对象

    function wait() {
        var tasks = function () {
            alert("执行完毕");
            var data = {
                name: "tom",
                age: 18
            }
            def.resolve(data); //改变deffered对象的状态
        };
        setTimeout(tasks, 2000);
        return def; //返回defferedu对象
    }

    $.when(wait())
        .done(function (ref) {
            alert('成功');
            console.dir(ref);//resolve中返回的数据
        })
        .fail(function(){
            alert("错误");
        });
</script>

def.resolved()表示将def的状态变为已完成,从而触发done方法,在resolve中可以传入参数,该参数将会在done中指定的函数的参数中被接收

当然还有对应的将状态变更为未完成的方法 reject触发fail方法

但是在上面的方法存在一个问题,那就是def是一个全局对象,所以它的执行状态是可以在外部改变的

<script>
   .....
   //重复上面的代码
   def.resolve()
</script>

那么执行这段代码的时候我们会发现现在会立即执行done方法,在编译时我们在尾部添加的代码会优先于wait方法执行,所以在执行时会立即执行done方法,为了避免这种情况,JQuery提供了deferred.promise()方法,该方法作用是,在原来的deferred对象上返回另一个deferred对象,后者只开放与改变执行状态无关的方法,屏蔽与改变状态有关的方法,从而使得执行状态不能被改变

<script>
    var def = $.Deferred(); //定义一个deferred对象

    function wait() {
        var tasks = function () {
            alert("执行完毕");
            var data = {
                name: "tom",
                age: 18
            }
            def.resolve(data); 
        };
        setTimeout(tasks, 2000);
        return def.promise()//将返回值改变为promise对象
    }
    
    $.when(wait()).....//同上案例
    def.resolve() //此时这行代码已经无效了
</script>

当然,最佳的方法是将def设置为函数wait内的局部变量,这样外部就无法获取了,也就不能改变状态了

var wait = function(dtd){
    var def = $.Deferred(); //在函数内部,新建一个deferred对象
    .....
    //同上案例代码
    return dtd.promise(); //当然,只返回dtd就可以了,为了保险我们还是将它转换为promise对象
  };
$.when(wait())... //同上案例代码

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

JQuery规定,$.Deferred()可以接收一个函数名作为参数,所生成的deferred对象将作为这个函数的默认参数

<script>
    var wait = function(def){

        var tasks = function(){
            alert("执行完毕!");
            def.resolve();
        };

        setTimeout(tasks,5000);
        return def.promise();
    };

    $.Deferred(wait)
        .done(function(){ alert("成功"); })
        .fail(function(){ alert("失败"); });
</script>

我们也可以直接在wait函数上部署deferred接口

<script>
    var def = $.Deferred();
    var wait = function(){

        var tasks = function(){
            alert("执行完毕!");
            def.resolve();
        };

        setTimeout(tasks,5000);
        return def.promise();
    };

    def.promise(wait);
    //这一行是关键代码,promise方法会将deferred的接口部署到wait函数上
    // 所以done方法能够使用done等方法
    wait.done(function () {
        alert("成功")
    })
    
    console.log(wait())
    //打印wait()可以看到现在的wait是一个deferred方法
</script>

当然还有一个方法,deferred.then(),该方法将done和fail结合到了一起,如果值传入一个参数,效果等同于done

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

在jQuery 1.8之前,then()只是.done().fail()写法的语法糖,两种写法是等价的。在jQuery 1.8之后,then()返回一个新的deferred对象,而done()返回的是原有的deferred对象。如果then()指定的回调函数有返回值,该返回值会作为参数,传入后面的回调函数

$.get("url_1")
       .then(function (res) {
           return $.get("url_2")
       })
       .then(function (res) {
           console.log(res); //返回$.get("url_2")的结果
       })

以上代码是需要特别注意的,如果我们使用的是done方法,那么返回的会一直是$.get("url_1")的结果

$.get("url_1")
       .done(function (res) {
           return $.get("url_2")
       })
       .done(function (res) {
           console.log(res); //返回$.get("url_1")的结果
       })

推荐阅读更多精彩内容