作者: Bryan Forbes
譯者: feijia (tiimfei@gmail.com)
原文連接: http://dojotoolkit.org/documentation/tutorials/1.6/deferreds/
適用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對象。