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")的结果
       })
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351