什么是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)
对象,这个对象没有像done
和fail
这样的回调方法。
之后的版本返回的是一个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
方法模拟了一个很耗时的操作,之后给这个操作指定了回调函数done
和fail
。一旦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请求成功和失败返回的信息不同,成功时为返回的数据,失败则为错误信息),最好只使用它的行为,而不检查其参数。如果要执行的操作和参数有关,请显示地指定done
和fail
回调。如下所示:
$.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,
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;
};
// 绑定回调函数
$.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
返回的对象上已经去掉了和改变状态有关的方法。notify
和notifyWith
是调用progress回调,resolve
和reject
用于设置其状态,带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对象作为参数,为其指定回调函数。
deferred.then的传递作用
之前我们介绍了,then
方法接一到三个参数,分别指定deferred
对象状态为已成功、已失败和继续等待的回调函数。
除此之外,then
还可以传递迟延对象。我们知道,deferred
可以链式操作的原因是其返回的仍是deferred
对象。then
中的回调函数如果没有返回新的deferred
对象时,将依然使用最开始的那个deferred
对象,如果在其也行返回一个deferred
对象时,之后的操作将被转移到在回调函数中return出来的新的deferred
对象,从而进行传递。
我们来看一个例子:
有5个js文件,其内容如下:
// perosn.js
var Person = function(name){
this.name= name;
};
// person.prototype.js
if (Person) {
Person.prototype.showName = function() {
return this.name;
};
} else {
console.error('Person is undefined!');
}
// zs.js
var zs = new Person('张三');
// ls.js
var ls = new Person('李四');
// introduce.js
var introduceEachOther = function() {
console.log('张三:你好,我叫' + zs.showName());
console.log('李四:你好,我叫' + ls.showName());
};
文件内容都非常简单,仅做演示使用,但是其中的依赖关系非常明显,person.prototype.js依赖于perosn.js,zs.js和ls.js依赖于person.js,introduce.js依赖于其他所有的js。
要分步加载,并保证可用,就必须保证在加载当前js时,其依赖的js已经加载完毕。
使用传统的写法,势必会嵌套多层,不仅逻辑不好处理,而且可读性很差。
我们用then
演示一下如何来传递递延对象来完成这个操作。
$.ajax({
url: 'person.js',
dataType: 'script'
}).then(function() {
console.log(Person); // person.js已经加载成功
console.log('person.js已经加载成功');
return $.ajax({
url: 'person.prototype.js',
dataType: 'script'
});
}).then(function(data) {
// 这里的data 上一步请求文件的内容 是person.prototype.js而非person.js
console.log(data);
console.log(Person.prototype.showName);
console.log('person.prototype.js已经加载成功');
// person.prototype.js 已经加载
return $.when($.ajax({
url: 'zs.js',
dataType: 'script'
}), $.ajax({
url: 'ls.js',
dataType: 'script'
}));
}).then(function(){
// zs.js 和 ls.js 都加载完成
console.log(zs,ls);
console.log('zs.js和zs.js已经加载成功');
return $.ajax({
url: 'introduce.js',
dataType: 'script'
});
},function(){
console.log('zs.js or ls.js failed');
}).then(function(){
// 到此前面的所有资源都是加载成功的
console.log('introduce.js已经加载成功,且其依赖资源都加载成功了,可以使用了');
introduceEachOther();
});
以上处理顺序只是举例演示,只要保证person.js最先加载,introduce.js最后加载,person.prototype.js在person.js之后即可。ls.js、zs.js与person.prototype.js的顺序可以调整。
我们每次在then
的第一个回调中返回了而一个新的递延对象,之后的操作就是基于你返回的那个递延对象了,第二个then
回调中输出的data证明了这一点。(如果没有return
出一个新的递延对象的话,依然使用之前的那个。)
参考链接:
本文是照着下面的文章加上自己的学习体会书写的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。