原文: http://dojotoolkit.org/documentation/tutorials/1.10/promises/index.html
版本: Dojo 1.10
Deferreds是一个神奇且功能强大的东西,是一个更伟大的东西Promises的实现。这里,我们将会学习它们的概念,以及其它一些在统一方式下同时使用promises和常规值Dojo's API。
学习这节的基础是要先学习dojo/request和dojo/Deferred知识,和这些接口的基础概念,我们将会介绍一个更加抽象的概念:promises。一个promise是一个对象,它表示一个操作结束后的最终返回值。dojo/promise接口在1.8版本后进行了重大的更新和改良。一个promise有如下特性:
> 可以在这三种状态之一的状态:unfulfilled, resolve, rejected
> 只可以从unfulfilled转变到resolved或unfulfilled转变到rejected状态
> 实现是一个then方法,用来注册提醒状态改变的回调函数
> 回调函数不能改变promise返回的修士
> 一个promise的then方法返回一个新的promise,用来在提供链式结构同时保持初始promise的值不变。
有了这些知识,我们来探究Dojo是如何实现promises的。
Defferred as a Promise
如果你觉得一个promise听起来更像是一个Deferred的话,说明你已经注意到这点了。事实上,dojo/Deferred模块是Dojo的promise接口的主要实现。如下例子:
require(['dojo/request'], function(request) { // original is a Deferred var original = request.get("users-mangled.json", { handleAs: "json" }); });正如之前所说,request.get(和其它Dojo的Ajax帮助函数)返回了一个promise,这个promise就表示从server端请求结束时返回的最终值。最初,它将会是一个unfulfilled状态,然后根据server返回的结果转变为resolved或rejected状态。
我们可以在请求返回的结果promise上通过then方法注册回调函数,但是,我们只能知道then方法的返回值有一个then方法,而并不能涵盖then方法返回值的全部。你可能认为它返回了初始promise,但是它实际上返回了一个实现promise接口的简单的对象。两个最常用的方法是then和cancel。下面是一个例子:
require(['dojo/_base/array', 'dojo/dom', 'dojo/dom-construct', 'dojo/json'], function(arrayUtil, dom, domConstruct, JSON) { // result is a new promise that produces a new value var result = original.then(function(response) { var userlist = dom.byId("userlist1"); return arrayUtil.map(response, function(user) { domConstruct.create("li", { innerHTML: JSON.stringify(user) }, userlist); return { id: user[0], username: user[1], name: user[2] }; }); }); });这个then调用返回了一个promise对象,这个promise对象的值将会被回调函数的返回值设定。我们能够看出,新的promise的值和最初的Deferred是不同的。
// chaining to the result promise rather than the original deferred to get our new value result.then(function(objs) { var userlist = dom.byId("userlist2"); arrayUtil.forEach(objs, function(user) { domConstruct.create("li", { innerHTML: JSON.stringify(user) }, userlist); }); });promise返回的值都是其回调函数的返回值,如果promise的回调函数并没有返回值,那这个promise的值将会是undefined。如果在你的chaining中出现了undefined,请确保你的promise的回调函数中提供了返回值。如果你不需要考虑chaining,那就不需要担心是否提供了返回值。
此时,我们可以检查最初的Deferred的值并没有改变:
// creating a list to show that the original deferred's value was untouched original.then(function(response) { var userlist = dom.byId("userlist3"); arrayUtil.forEach(response, function(user) { domConstruct.create("li", { innerHTML: JSON.stringify(user) }, userlist); }); });正如我们之前所见,chaining是非常强大的,它更强大的地方在于chain中的每个对象是不可变的。
需要注意的是,Deferred实例包括另外一个属性:promise,这是一个只实现了promise接口的对象,但是代表了Deferred的返回值。该promise属性允许你最小化使用者调用你的接口所带来的负面影响,通过避免有意或无意调用resolve或reject方法,但是仍然允许他们获取最初Deferred的值。
dojo/when
dojo/when是Dojo提供的一个强大的函数,它允许你用一致的接口处理promises或常规值。dojo/when函数具有4个参数:一个promise或常规值,一个可选回调函数,一个可选错误处理函数,和一个可选过程(progress)函数。它执行以下两种情况之一:
> 如果第一个参数不是一个promise,且提供了回调函数,第一个参数的值将传递给这个回调函数并立即执行,并返回回调函数的执行结果。如果没有提供回调函数,那第一个参数值将被立即返回。
> 如果第一个参数是一个promise,在promise的then方法中提供了回调函数、错误处理函数和过程(progress)函数,将会返回一个新的promise。设定回调函数用来在promise完成时执行。
下面是一个例子:
function getUserList() { return request.get("users-mangled.json", { handleAs: "json" }).then(function(response) { return arrayUtil.map(response, function(user) { return { id: user[0], username: user[1], name: user[2] }; }); }); }假设用户列表不会经常改变,且可以在client端缓存而不是每次函数调用时都去获取它们。在这个情形中,因为dojo/when需要一个常规值或一个promise,getUserList可以改为返回一个promise或者一个用户的数组,然后,我们就可以用dojo/when来处理这个返回值:
require(['dojo/_base/array', 'dojo/when', 'dojo/request', 'dojo/dom', 'dojo/dom-construct', 'dojo/json'], function(arrayUtil, when, request, dom, domConstruct, JSON) { var getUserList = (function() { var users; return function() { if(!users) { return request.get("users-mangled.json", { handleAs: "json" }).then(function(response) { // Save the resulting array into the users variable users = arrayUtil.map(response, function(user) { return { id: user[0], username: user[1], name: user[2] }; }); // Make sure to return users here, for valid chaining return users; }); } return users; }; })(); }); when(getUserList(), function(users) { // This callback will be run after the request completes var userlist = dom.byId("userlist1"); arrayUtil.forEach(users, function(user) { domConstruct.create("li", { innerHTML: JSON.stringify(user) }, userlist); }); when(getUserList(), function(user) { // This callback will run right away since it's already in cache var userlist = dom.byId("userlist2"); arrayUtil.forEach(users, function(user) { domConstruct.create("li", { innerHTML: JSON.stringify(user) }, userlist); }); }); });也可以是你负责用来创建用户列表的API,并想为你的开发者提供一个清析的API来从server端或一个数组传递给你一个用户列表。这种情况下,你可能会用如下的一个方法:
function createUserList(node, users){ var nodeRef = dom.byId(node); return when( users, function(users){ arrayUtil.forEach(users, function(user){ domConstruct.create("li", { innerHTML: JSON.stringify(user) }, nodeRef); }); }, function(error){ domConstruct.create("li", { innerHTML: "Error: " + error }, nodeRef); } ); } var users = request.get("users-mangled.json", { handleAs: "json" }).then(function(response){ return arrayUtil.map(response, function(user){ return { id: user[0], username: user[1], name: user[2] }; }); }); createUserList("userlist1", users); createUserList("userlist2", [{ id: 100, username: "username100", name: "User 100" }]);如上,dojo/when允许开发者用一个接口同时处理同步和异步情形,同时在生产者和消费者范围。
用dojo/promise/all处理promises列表
dojo/promise/all代替了dojo/DeferredList模块,通过结合多个promises结果为一个promise提供了处理多个异步操作机制。有时,你需要并行获取多个源的数据,并想在所有请求都结束时获得一个通知,dojo/promise/all就提供了这样的解决方法。
dojo/promise/all的使用很简单,只要向它的构造函数传递一个对象或Deferreds的数组,返回结果是一个使用在传递参数中相同键的对象,或者一个和传入数组相同顺序的数组,下面是一个说明例子:
require(["dojo/promise/all", "dojo/Deferred", "dojo/request", "dojo/_base/array", "dojo/dom-construct", "dojo/dom", "dojo/json", "dojo/domReady!"], function(all, Deferred, request, arrayUtil, domConstruct, dom, JSON){ var usersDef = request.get("users.json", { handleAs: "json" }).then(function(response){ var users = {}; arrayUtil.forEach(response, function(user){ users[user.id] = user; }); return users; }); var statusesDef = request.get("statuses.json", { handleAs: "json" }); all([usersDef, statusesDef]).then(function(results){ var users = results[0], statuses = results[1], statuslist = dom.byId("statuslist"); if(!results[0] || !results[1]){ domConstruct.create("li", { innerHTML: "An error occurred" }, statuslist); return; } arrayUtil.forEach(statuses, function(status){ var user = users[status.userId]; domConstruct.create("li", { id: status.id, innerHTML: user.name + ' said, "' + status.status + '"' }, statuslist); }); }); });这里,我们想从server端同时获取用户列表和状态列表,通过注册一个回调函数返回用户ID的hash,将两个Deferreds都传递给dojo/promise/all,并为其注册了一个回调函数,该回调函数检查错误,如果没有错误,就会遍历状态数组,将其和用户匹配起来。无论哪个请求先执行结束,dojo/promise/all都会返回一个Deferreds传入时的顺序的数组。
总结
Dojo的promises接口为开发者提供了两种创建更加强大的应用的机会:由于Deferred函数返回的promises是不可变的避免了一些负面影响,且dojo/when提供了一个跨越基于promise和基于常规值编码的鸿沟。基于这个,dojo/promise/all允许你用一个回调函数处理多个deferreds/promises。