适用dojo 版本: 1.6
难度: 中等
在本教程中, 你将学到使用Dojo的 defer(延迟) 方法的基本知识。
使用延迟方法,可以优雅轻松的处理在JS编程中常见的异步调用。我们还将解释如何使用DeferredList,它能够一次处理多个延迟的异步调用。
入门
初听起来, “延迟" (Deferred) 这个名字颇为神秘。但它其实是一个Dojo提供给我们用来处理异步调用(例如Ajax)的强大工具. 简单来说,一个Deferred对象它会等待一段时间再去执行指定的调用,直到某个特定的事件发生或者前一个动作完成。Ajax就是一种常见的例子: 发出一个Ajax请求之后,我们需要等待服务器把响应返回之后才会调用处理函数。所以Deferred对象提供给我们的就是这种将某个动作延迟并等待某个事件发生的能力。在本教程中,我们将会结合Ajax教程的内容一起来解读如何使用Deferred对象更好的管理异步的JS程序。
dojo.Deferred
延迟的概念并非Dojo专有,(译者:Jquery等其他JS框架也有类似的对象和实现, CommonJS 组织还为此定义了一套 标准), 从0.3 版本开始Dojo就实现了dojo.Deferred对象。
Deferred对象有三个状态,初始化时是"unresolve” 状态,当它所等待的事件发生时, 进入"resolve" 状态, 第三种状态是出错状态,即该事件没有按照预期的发展,例如服务器端返回了错误消息,也称reject 状态.
创建Deferred对象后,我们可以通过调用该对象的then方法注册一个回调函数,表示当这个Deferred对象等待的某个事件发生时(resolve),就调用该回调函数。then 方法还接受第二个参数,可以制定当事件失败或出错时(reject)时调用的出错回调函数 .
让我们来看一个例子:
var def = new dojo.Deferred(),
userlist = dojo.byId("userlist");
def.then(function(res){
// This will be called when the deferred
// is resolved
dojo.forEach(res, function(user){
dojo.create("li", {
id: user.id,
innerHTML: user.username + ": " + user.name
}, userlist);
});
},function(err){
// This will be called when the deferred
// is rejected
dojo.create("li", {
innerHTML: "Error: " + err
}, userlist);
});
dojo.xhrGet({
url: "users.json",
handleAs: "json",
load: function(res){
// Resolve when content is received
def.resolve(res);
},
error: function(err){
// Reject on error
def.reject(err);
}
});
查看示例
在上面的示例中,我们创建了一个dojo.Deferred 对象并在上面分别注册了一个成功回调函数和出错回调函数。我们还调用了dojo.xhrGet 一个异步Ajax调用,去服务器端获取"user.json"。 如果这个ajax调用成功了,我们在xhr对象的load属性所设的回调函数中会将dojo.Deferred对象置为resolve状态 ,这时我们在该Deferred对象上注册的回调函数将会被调用;如果ajax调用失败,则Deferred上注册的错误回调函数将会被调用。
上面的例子,你可能会觉得是多此一举, 为什么不直接在xhrGet里直接分别设定成功和失败的回调函数呢? 是的,你的确可以这么做,但是通过引入Deffered对象,我们把负责处理服务器端返回数据的逻辑(回调函数)和发送Ajax请求的逻辑进行了解藕。
实际上,为了方便开发者使用Deffered对象,Dojo的Ajax构造函数方法会直接返回给你一个Deferred对象,因此上面的代码可以简化不少:
var def = dojo.xhrGet({
url: "users.json",
handleAs: "json"
});
def.then(function(res){
var userlist = dojo.byId("userlist");
dojo.forEach(res, function(user){
dojo.create("li", {
id: user.id,
innerHTML: user.username + ": " + user.name
}, userlist);
});
},function(err){
// This shouldn't occur, but it's defined just in case
alert("An error occurred: " + err);
});
查看示例
在这个例子中我们不再需要设置dojo.xhrGet的 load属性了,可以直接在xhrGet返回的Deferred对象上通过then来注册回调函数. 代码逻辑更加直观简洁.
在回调函数中,我们遍历了从服务器端返回的用户列表,并且为每个用户创建了一个HTML列表 。 从功能上看,和前一个例子完全一样,但是在这个例子中,我们得以把处理服务器端数据的逻辑和发送Ajax请求的逻辑分开了。 所以Deferred对象的一个重要功能就是对我们的程序逻辑进行解藕。 (decoupling)
链式调用
dojo.Deferred是个挺容易理解的概念,但是它还有一些很强大的功能值得我们继续探索. 其中之一就是链式调用(Chaining):每个then方法的返回值都仍然是一个Defered对象。 我们来看一个例子:
假设前面的例子里服务器端返回的不是JSON格式的用户对象,而是每个用户的信息的原始值。 值当然不如对象方便使用,所以我们希望注册一个回调函数来把这些原始数值转换为用户对象。
var original = dojo.xhrGet({
url: "users-mangled.json",
handleAs: "json"
});
var result = original.then(function(res){
var userlist = dojo.byId("userlist1");
return dojo.map(res, function(user){
dojo.create("li", {
innerHTML: dojo.toJson(user)
}, userlist);
return {
id: user[0],
username: user[1],
name: user[2]
};
});
});
// 由original的then方法返回的result对象也有一个`then` 方法来接受回调函数,和original对象一样。
// 但是要注意的是传给result.then中注册的回调函数的值,不是Ajax调用获取的数据, 而是original的回调函数的返回值。
// 也就是已经经过格式转换的用户对象map
result.then(function(objs){
var userlist = dojo.byId("userlist2");
dojo.forEach(objs, function(user){
dojo.create("li", {
innerHTML: dojo.toJson(user)
}, userlist);
});
});
注意: 严格来说then方法的返回值并不是一个Deferred对象,它有个特定的名字"promise", 即承诺,实现了一个特定的API. 你可以进一步阅读关于prommise的教程来深入学习, 不过在这里,我们可以暂时理解为 一个promise对象提供了和Deferred对象完全相同的then方法。 因此Deferred对象和Promise对象的then方法可以进行连续的链式调用。这样做的好处是什么呢?链式调用时,原始的Defered对象不会被修改,而且服务器端的返回的数据也没有被修改,你可以继续在original的defered对象上注册其他的回调函数来对原始数据进行进一步操作,在前面的例子基础上,你可以注册一个新的回调到original上,例如:
original.then(function(res){
var userlist = dojo.byId("userlist3");
dojo.forEach(res, function(user){
dojo.create("li", {
innerHTML: dojo.toJson(user)
}, userlist);
});
});
查看示例
我们进一步可以把上面的例子改成:
function getUserList(){
// 注意我们这里不是返回xhrGet获取到的数据,
// but of the .then call on that xhrGet's return
return dojo.xhrGet({
url: "users-mangled.json",
handleAs: "json"
}).then(function(res){
return dojo.map(res, function(user){
return {
id: user[0],
username: user[1],
name: user[2]
};
});
});
}
getUserList().then(function(users){
var userlist = dojo.byId("userlist");
dojo.forEach(users, function(user){
dojo.create("li", {
innerHTML: dojo.toJson(user)
}, userlist);
});
});
通过这样封装, 使用getUserList的方法就总是能获取到一个已经处理好的用户列表了。 (实验: 反复调用会导致多次ajax调用么?)
dojo.DeferredList
有时,我们需要同时从多个不同的来源获取数据,当这些数据全部到位后我们希望可以被通知到。Dojo也提供了方便的封装来辅助你完成这一工作,这就是dojo.DeferredList对象。
使用dojo.DeferredList时, 只要把一组Deferred对象(作为数组)传入它的构造函数,它会返回给你一个新的Deferred对象。 在此对象上,你可以注册一个回调函数,当该回调函数被调用时,原始的deferred对象的结果会作为参数被传递进入该回调函数。参数是一个tuple数组,(tuple就是一个二元数值对)。数值对中第一个数值是一个boolean表示该Deferred是成功还是失败,第二个数值是该Deferred的返回值。 让我们看一个例子:
dojo.require("dojo.DeferredList");
dojo.ready(function(){
// 第一个Ajax请求,产生一个defferred 对象: userDef
var usersDef = dojo.xhrGet({
url: "users.json",
handleAs: "json"
}).then(function(res){
var users = {};
dojo.forEach(res, function(user){
users[user.id] = user;
});
return users;
});
// 另一个Ajax请求,产生第二个defferred 对象: statusesDef
var statusesDef = dojo.xhrGet({
url: "statuses.json",
handleAs: "json"
});
//利用两个Defferred对象构造一个DefferredList对象
var defs = new dojo.DeferredList([usersDef, statusesDef]);
//DeferredList 对象也有一个then方法用来注册回调函数,回调函数的参数是一个tuple构成的数组
// 该回调函数只有当前DefferredList所包含的所有Deferred对象进入Resolved或者Error状态后才会调用。
defs.then(function(results){
// 每个tuple的第二个值,是相对应的deferred注册的回调函数的返回值,
var users = results[0][1],
statuses = results[1][1],
statuslist = dojo.byId("statuslist");
// 每个tuple的第一个值,是一个boolean,表示该Deffered对象所代表的请求是否成功了 即deffered 是否成功resolved
if(!results[0][0] || !results[1][0]){
dojo.create("li", {
innerHTML: "An error occurred"
}, statuslist);
return;
}
dojo.forEach(statuses, function(status){
var user = users[status.userId];
dojo.create("li", {
id: status.id,
innerHTML: user.name + ' said, "' + status.status + '"'
}, statuslist);
});
});
});
这个例子中我们想从服务器端分别获取用户信息和用户的状态。 我们使用了一个DeferrredList来等待这两个请求都完成后进行处理,回调函数先检查两个请求是否都成功完成了,如果没有发生任何错误,则遍历两个请求分别获得的用户数据和状态数据。 回调函数只有当两个请求都进入完成状态后才会被调用,因此我们不用关心究竟是哪个请求先完成了。
(设想一下,如果没有DeferredList,你需要如何手动处理这个情形?)
查看示例
结论
绝大多数JavaScript应用都需要使用Ajax,因此它们都需要注册异步的回调函数。dojo.Deferred就提供了这样一个简单又优雅的方法。 它可以非常灵活的进行链式调用;而使用dojo.DeferredList又可以让你处理个Deferred对象。