手记

jQuery Deferred 对象

什么是deferred对象

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

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

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

它解决了如何处理耗时操作的问题,对那些操作提供了更好的控制,以及统一的编程接口。它的主要功能,可以归结为四点。

ajax的链式写法

我们来看一个$.ajax请求的例子:

$.ajax({
    url: 'test.jspx',
    success: function(data) {
        // 请求成功了
        // TODO
    },
    error: function(error) {
        // 请求失败了
        // TODO
    }
});

上面的代码再常见不过了,不过下面的写法你也一定看过:

$.ajax('test.jspx')
    .done(function(data) {
        // 请求成功了
        // TODO
    })
    .fail(function(error) {
        // 请求失败了
        // TODO
    });

$.ajax({
    url: 'test.jspx',
    type:'POST',
    data: data
}).done(function(data) {
    // 请求成功了
    // TODO
}).fail(function(error) {
    // 请求失败了
    // TODO
});

其实在1.5.0版本的之前的jquery是不支持链式写法的,只能使用第一种写法,原因是此版本之前的jquery的ajax操作返回的是一个XHR (XMLHttpRequest) 对象,这个对象没有像donefail这样的回调方法。

之后的版本返回的是一个deferred对象用promise方法包装过的对象,可以进行链式操作,使用链式写法后,代码可读性大大提高。

为一个操作指定多个回调函数

如果要在一个ajax请求成功后再执行别的回调函数,该怎么办呢? 直接在加在后面就可以了:

$.ajax('test.jspx')
    .done(function(data) {
        // 请求成功了
        // TODO
    }).fail(function(error) {
        // 请求失败了
        // TODO
    }).done(function(data) {
        // 请求成功了
        // then TODO
    });

这种写法可以支持无数个回调函数,这写回调函数将按照添加顺序依次执行。

为多个操作指定回调函数

如果一个回调函数需要在几个ajax请求都成功后才能执行该怎么办呢?是不是不好控制呢?其实jQuery提供了这样一个方法$.when() 它可以接收任意个deferred对象,只有所有的deferred对象都状态都为成功时才执行done回调,否则执行fail回调。

$.when($.ajax("test1.html"), $.ajax("test2.html"))
    .done(function() {
        // 两个操作都请求都成功
        // TODO
    }).fail(function() {
        // 任意一个失败
        // TODO
    });

上面代码中,只有两个请求都成功后才会执行done回调,只要有一个失败就执行fail回调。

给普通的操作指定回调函数

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

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

// 绑定回调函数                 
$.when(wait())
    .done(function() {
        alert("执行成功了!");
    })
    .fail(function() {
        alert("出错啦!");
    });

上面代码的中的wait方法模拟了一个很耗时的操作,之后给这个操作指定了回调函数donefail。一旦wait执行完毕就会立即调用done这个回调函数。

Deferred对象的常用方法

上面已经对deferred对象有所了解了,下面介绍一下deferred对象的常用方法。

deferred.resolve()

在jQuery的deferred对象中,规定了它有三种状态:

未完成 继续等待

已完成 立即调用done回调

已失败 立即调用fail回调

resolve方法的作用就是设置deferred对象状态为已完成,deferred对象立刻调用done()方法指定的回调函数

deferred.reject()

reject方法的作用是设置deferred对象状态为已失败,deferred对象立刻调用fail()方法指定的回调函数

deferred.done()

done方法用于指定deferred对象状态为已完成时的回调函数。

deferred.fail()

done方法用于指定deferred对象状态为已失败时的回调函数。

deferred.then()

then方法接收一到三个参数,分别指定deferred对象状态为已成功、已失败和继续等待的回调函数。

deferred.always()

always方法用于指定deferred对象状态为已成功或已失败时的回调函数。

即无论这个deferred对象是成功还是失败,只要执行完毕都会调用此方法指定的回调。

由于此方法指定的回调函数的参数是不确定的(比如ajax请求成功和失败返回的信息不同,成功时为返回的数据,失败则为错误信息),最好只使用它的行为,而不检查其参数。如果要执行的操作和参数有关,请显示地指定donefail回调。如下所示:

$.ajax('test1.html')
    .always(function() {
        // 不管请求是否成功,只要请求完毕就执行
        // TODO
        console.log('已请求完毕,状态未知');
    });

$.ajax('test1.html')
    .done(function(data) {
        // 请求成功时执行
        // TODO
        console.log('请求已成功,返回数据为:');
        console.log(data);
    })
    .fail(function(error) {
        // 请求失败时执行
        // TODO
        console.log('请求已失败,错误信息:');
        console.log(error.status, error.statusText);
    });

deferred.progress()

progress方法用于指定deferred对象状态为等待中的回调函数。但是它仅在deferred对象生成了进度通知时才会被调用。

请看下面例子:

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

// 绑定回调函数                 
$.when(wait())
    .done(function() {
        alert("执行成功了!");
    })
    .fail(function() {
        alert("出错啦!");
    })
    .progress(function(){
        console.log("正在执行中..."); // 此处不会有任何输出
    });

上面虽然指定了progress回调,但是却为没有任何作用的原因是由于在deferred对象没有生成进度通知,所以其不会被调用。

想要progress回调能执行,需要在deferred对象上调用此回调。notify方法的作用就是根据给定的 args参数 调用Deferred对象上进行中的progress回调。

var wait = function() {
    var dtd = $.Deferred(); // 新建一个deferred对象   
    var i = 1,
        percent,            // 记录进度
        showPercent = function() {
            percent = (i * 500) / 5000 * 100 + '%';
            dtd.notify(percent); // 调用progress回调
            i++;
        };
    showPercent();          // 首先执行一次 因为setInterval是迟延后才执行第一次
    setInterval(showPercent, 500);

    var tasks = function() {      
        alert("执行完毕!");      
        dtd.resolve();      // 此操作完成后改变deferred对象的执行状态                          
    };    
    setTimeout(tasks, 5000);    
    return dtd;  
};

// 绑定回调函数                 
$.when(wait())
    .done(function() {
        alert("执行成功了!");
    })
    .fail(function() {
        alert("出错啦!");
    })
    .progress(function(data) {
        console.log('执行中,已完成', data);
    });
// 执行中,已完成 10%
// 执行中,已完成 20%
// 执行中,已完成 30%
// 执行中,已完成 40%
// 执行中,已完成 50%
// 执行中,已完成 60%
// 执行中,已完成 70%
// 执行中,已完成 80%
// 执行中,已完成 90%
// 执行中,已完成 100%
// 之后弹出 执行完毕!和 执行成功了!

这个方法给上传文件或者耗时操作生成进度条提供了一种可能。

jQuery3.0以上版本对when方法做了大幅调整。向promise/A+靠齐,上面的写法中notify是触发不了when中的progress回调的,需要使用promise来给对象部署deferred接口或使用$.Deferred()传入函数名。

简而言之,3.0以以上版本中,上面代码中progress回调是不会进去的,应使用以下写法:

1、promise给一个对象部署Deferred接口:

var dtd = $.Deferred(); // 新建一个deferred对象
var wait = function(dtd) {
    var i = 1,
        timer,
        percent; // 记录进度
    var tasks = function() {
        if (i == 11) {
            alert("执行完毕!");
            dtd.resolve(); // 此操作完成后改变deferred对象的执行状态
        } else {
            percent = (i * 500) / 5000 * 100 + '%';
            dtd.notify(percent); // 调用progress回调
            i++;
            setTimeout(tasks, 500);
        }
    };
    setTimeout(tasks, 1000);        
};
// 在wait对象上部署Deferred接口,此后就可以直接在wait上使用deferred对象promise后的方法了
dtd.promise(wait);
// 在wait对象上使用deferred对象的方法指定回调。
wait.done(function() {
    alert("执行成功了!");
})
.fail(function() {
    alert("出错啦!");
})
.progress(function(data) {
    console.log('执行中,已完成', data);
});
// 执行
wait(dtd);

2、使用$.Deferred传入函数名:

var wait = function(dtd) {
    var i = 1,
        timer,
        percent; // 记录进度

    var tasks = function() {
        if (i == 11) {
            alert("执行完毕!");
            dtd.resolve(); // 此操作完成后改变deferred对象的执行状态
        } else {
            percent = (i * 500) / 5000 * 100 + '%';
            dtd.notify(percent); // 调用progress回调
            i++;
            setTimeout(tasks, 500);
        }
    };
    setTimeout(tasks, 1000);
    return dtd;
};    
// 绑定回调函数
$.Deferred(wait)
    .done(function() {
        alert("执行成功了!");
    })
    .fail(function() {
        alert("出错啦!");
    })
    .progress(function(data) {
        console.log('执行中,已完成', data);
    });

deferred.promise()

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;  
};  
$.when(wait(dtd))  
    .done(function() {
        alert("执行成功!");
    })  
    .fail(function() {
        alert("出错啦!");
    });
dtd.reject(); // 改变状态为失败,将立即触发fail 5s后完成再出发done

如果我们把deferred对象定义在了函数外部,那么我们设置deferred对象的状态就会导致调用对应的回调。上面代码中,最后调用reject方法,会导致立即调用了fail回调,5s之后又弹出执行完毕和执行成功。这将会导致不必要的混乱。使用promise方法就是一种解决方案。(之前写的将var dtd = $.Deferred()放在函数内部,使得外部访问不到也是一种解决方案)。

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(); // d.resolve is not a function 经过promise后没有resolve方法了

我们看一下Deferred对象和它promise之后的区别。

promise返回的对象上已经去掉了和改变状态有关的方法。notifynotifyWith是调用progress回调,resolvereject用于设置其状态,带with的方法可指定上下文环境。

此方法还可以接收Object类型的参数,deferred.promise()会将事件绑定到该参数上,然后返回该对象,而不是创建一个新的对象。 这个方法可以用于在已经存在的对象上绑定 Promise 行为的情况。示例如下:

var dtd = $.Deferred(); // 生成Deferred对象
  
var wait = function(dtd) {    
    var tasks = function() {      
        alert("执行完毕!");      
        dtd.resolve(); // 执行完毕后改变Deferred对象的执行状态
            
    };    
    setTimeout(tasks, 5000);  
};   
// 在wait对象上部署Deferred接口,此后就可以直接在wait上使用deferred对象promise后的方法了
dtd.promise(wait);
// 在wait对象上使用deferred对象的方法指定回调。
wait.done(function() {
    alert("哈哈,成功了!");
})  .fail(function() {
    alert("出错啦!");
});  
// 执行
wait(dtd);

$.Deferred()

$.Deferred()除了创建一个deferred对象之外,可以接受一个函数名(注意,是函数名)作为参数,$.Deferred()所生成的deferred对象将作为这个函数的默认参数。

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

$.Deferred(wait)
    .done(function() {
        alert("执行成功!");
    })
    .fail(function() {
        alert("执行失败!");
    });

$.when()

接收一个或多个deferred对象作为参数,为其指定回调函数。

见 [为多个操作指定回调函数]

参考链接:

本文是照着下面的文章加上自己的学习体会书写的。

阮一峰:jQuery的deferred对象详解

10人推荐
随时随地看视频
慕课网APP

热门评论

更新了 deferred.progress()

查看全部评论