dojo.Deferred(延遲)初探

作者: Bryan Forbes

譯者: feijia ([email protected])

原文連接: 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)時調用的出錯回調函數 . 

 

讓我們來看一個例子:

 

  1. var def = new dojo.Deferred(),  
  2.     userlist = dojo.byId("userlist");  
  3.    
  4. def.then(function(res){  
  5.     // This will be called when the deferred  
  6.     // is resolved  
  7.     dojo.forEach(res, function(user){  
  8.         dojo.create("li", {  
  9.             id: user.id,  
  10.             innerHTML: user.username + ": " + user.name  
  11.         }, userlist);  
  12.     });  
  13. },function(err){  
  14.     // This will be called when the deferred  
  15.     // is rejected  
  16.     dojo.create("li", {  
  17.         innerHTML: "Error: " + err  
  18.     }, userlist);  
  19. });  
  20.    
  21. dojo.xhrGet({  
  22.     url: "users.json",  
  23.     handleAs: "json",  
  24.     load: function(res){  
  25.         // Resolve when content is received  
  26.         def.resolve(res);  
  27.     },  
  28.     error: function(err){  
  29.         // Reject on error  
  30.         def.reject(err);  
  31.     }  
  32. });  

 

 

查看示例

 

在上面的示例中,我們創建了一個dojo.Deferred 對象並在上面分別註冊了一個成功回調函數和出錯回調函數。我們還調用了dojo.xhrGet 一個異步Ajax調用,去服務器端獲取"user.json"。 如果這個ajax調用成功了,我們在xhr對象的load屬性所設的回調函數中會將dojo.Deferred對象置爲resolve狀態 ,這時我們在該Deferred對象上註冊的回調函數將會被調用;如果ajax調用失敗,則Deferred上註冊的錯誤回調函數將會被調用。 

 

 

上面的例子,你可能會覺得是多此一舉, 爲什麼不直接在xhrGet裏直接分別設定成功和失敗的回調函數呢? 是的,你的確可以這麼做,但是通過引入Deffered對象,我們把負責處理服務器端返回數據的邏輯(回調函數)和發送Ajax請求的邏輯進行了解藕。

 

實際上,爲了方便開發者使用Deffered對象,Dojo的Ajax構造函數方法會直接返回給你一個Deferred對象,因此上面的代碼可以簡化不少:

 

  1. var def = dojo.xhrGet({  
  2.     url: "users.json",  
  3.     handleAs: "json"  
  4. });  
  5.    
  6. def.then(function(res){  
  7.     var userlist = dojo.byId("userlist");  
  8.    
  9.     dojo.forEach(res, function(user){  
  10.         dojo.create("li", {  
  11.             id: user.id,  
  12.             innerHTML: user.username + ": " + user.name  
  13.         }, userlist);  
  14.     });  
  15. },function(err){  
  16.     // This shouldn't occur, but it's defined just in case  
  17.     alert("An error occurred: " + err);  
  18. });  

 

 

查看示例

 

在這個例子中我們不再需要設置dojo.xhrGet的 load屬性了,可以直接在xhrGet返回的Deferred對象上通過then來註冊回調函數. 代碼邏輯更加直觀簡潔. 

 

在回調函數中,我們遍歷了從服務器端返回的用戶列表,並且爲每個用戶創建了一個HTML列表 。 從功能上看,和前一個例子完全一樣,但是在這個例子中,我們得以把處理服務器端數據的邏輯和發送Ajax請求的邏輯分開了。 所以Deferred對象的一個重要功能就是對我們的程序邏輯進行解藕。 (decoupling)

 

鏈式調用

 

dojo.Deferred是個挺容易理解的概念,但是它還有一些很強大的功能值得我們繼續探索. 其中之一就是鏈式調用(Chaining):每個then方法的返回值都仍然是一個Defered對象。 我們來看一個例子:

 

假設前面的例子裏服務器端返回的不是JSON格式的用戶對象,而是每個用戶的信息的原始值。 值當然不如對象方便使用,所以我們希望註冊一個回調函數來把這些原始數值轉換爲用戶對象。

  1. var original = dojo.xhrGet({  
  2.              url: "users-mangled.json",  
  3.              handleAs: "json"  
  4.          });  
  5.            
  6.          var result = original.then(function(res){  
  7.              var userlist = dojo.byId("userlist1");  
  8.                
  9.              return dojo.map(res, function(user){  
  10.                  dojo.create("li", {  
  11.                      innerHTML: dojo.toJson(user)  
  12.                  }, userlist);  
  13.                    
  14.                  return {  
  15.                      id: user[0],  
  16.                      username: user[1],  
  17.                      name: user[2]  
  18.                  };  
  19.              });  
  20.          });  
  21.            
  22.          // 由original的then方法返回的result對象也有一個`then` 方法來接受回調函數,和original對象一樣。  
  23.          // 但是要注意的是傳給result.then中註冊的回調函數的值,不是Ajax調用獲取的數據, 而是original的回調函數的返回值。   
  24.          // 也就是已經經過格式轉換的用戶對象map  
  25.          result.then(function(objs){  
  26.              var userlist = dojo.byId("userlist2");  
  27.                
  28.              dojo.forEach(objs, function(user){  
  29.                  dojo.create("li", {  
  30.                      innerHTML: dojo.toJson(user)  
  31.                  }, userlist);  
  32.              });  
  33.          });  
  34.            

 

注意: 嚴格來說then方法的返回值並不是一個Deferred對象,它有個特定的名字"promise", 即承諾,實現了一個特定的API. 你可以進一步閱讀關於prommise的教程 來深入學習, 不過在這裏,我們可以暫時理解爲 一個promise對象提供了和Deferred對象完全相同的then方法。 因此Deferred對象和Promise對象的then方法可以進行連續的鏈式調用。這樣做的好處是什麼呢?鏈式調用時,原始的Defered對象不會被修改,而且服務器端的返回的數據也沒有被修改,你可以繼續在original的defered對象上註冊其他的回調函數來對原始數據進行進一步操作,在前面的例子基礎上,你可以註冊一個新的回調到original上,例如:

  1. original.then(function(res){  
  2.     var userlist = dojo.byId("userlist3");  
  3.    
  4.     dojo.forEach(res, function(user){  
  5.         dojo.create("li", {  
  6.             innerHTML: dojo.toJson(user)  
  7.         }, userlist);  
  8.     });  
  9. });  

 

 

 

查看示例

 

 

我們進一步可以把上面的例子改成:

 

  1. function getUserList(){  
  2.     // 注意我們這裏不是返回xhrGet獲取到的數據,  
  3.     // but of the .then call on that xhrGet's return  
  4.     return dojo.xhrGet({  
  5.             url: "users-mangled.json",  
  6.             handleAs: "json"  
  7.     }).then(function(res){  
  8.         return dojo.map(res, function(user){  
  9.             return {  
  10.                 id: user[0],  
  11.                 username: user[1],  
  12.                 name: user[2]  
  13.             };  
  14.         });  
  15.     });  
  16. }  
  17. getUserList().then(function(users){  
  18.     var userlist = dojo.byId("userlist");  
  19.     dojo.forEach(users, function(user){  
  20.         dojo.create("li", {  
  21.             innerHTML: dojo.toJson(user)  
  22.         }, userlist);  
  23.     });  
  24. });  

 

 

通過這樣封裝, 使用getUserList的方法就總是能獲取到一個已經處理好的用戶列表了。  (實驗: 反覆調用會導致多次ajax調用麼?)

 

dojo.DeferredList

 

有時,我們需要同時從多個不同的來源獲取數據,當這些數據全部到位後我們希望可以被通知到。Dojo也提供了方便的封裝來輔助你完成這一工作,這就是dojo.DeferredList對象。

 

使用dojo.DeferredList時, 只要把一組Deferred對象(作爲數組)傳入它的構造函數,它會返回給你一個新的Deferred對象。 在此對象上,你可以註冊一個回調函數,當該回調函數被調用時,原始的deferred對象的結果會作爲參數被傳遞進入該回調函數。參數是一個tuple數組,(tuple就是一個二元數值對)。數值對中第一個數值是一個boolean表示該Deferred是成功還是失敗,第二個數值是該Deferred的返回值。 讓我們看一個例子:

 

  1. dojo.require("dojo.DeferredList");  
  2. dojo.ready(function(){  
  3.     // 第一個Ajax請求,產生一個defferred 對象: userDef  
  4.     var usersDef = dojo.xhrGet({  
  5.         url: "users.json",  
  6.         handleAs: "json"  
  7.     }).then(function(res){  
  8.         var users = {};  
  9.         dojo.forEach(res, function(user){  
  10.             users[user.id] = user;  
  11.         });  
  12.         return users;  
  13.     });  
  14.    // 另一個Ajax請求,產生第二個defferred 對象: statusesDef  
  15.     var statusesDef = dojo.xhrGet({  
  16.         url: "statuses.json",  
  17.         handleAs: "json"  
  18.     });  
  19.     //利用兩個Defferred對象構造一個DefferredList對象  
  20.     var defs = new dojo.DeferredList([usersDef, statusesDef]);  
  21.     //DeferredList 對象也有一個then方法用來註冊回調函數,回調函數的參數是一個tuple構成的數組  
  22.     // 該回調函數只有當前DefferredList所包含的所有Deferred對象進入Resolved或者Error狀態後纔會調用。  
  23.     defs.then(function(results){  
  24.         // 每個tuple的第二個值,是相對應的deferred註冊的回調函數的返回值,   
  25.         var users = results[0][1],  
  26.             statuses = results[1][1],  
  27.             statuslist = dojo.byId("statuslist");  
  28.        // 每個tuple的第一個值,是一個boolean,表示該Deffered對象所代表的請求是否成功了 即deffered 是否成功resolved  
  29.         if(!results[0][0] || !results[1][0]){  
  30.             dojo.create("li", {  
  31.                 innerHTML: "An error occurred"  
  32.             }, statuslist);  
  33.             return;  
  34.         }  
  35.         dojo.forEach(statuses, function(status){  
  36.             var user = users[status.userId];  
  37.             dojo.create("li", {  
  38.                 id: status.id,  
  39.                 innerHTML: user.name + ' said, "' + status.status + '"'  
  40.             }, statuslist);  
  41.         });  
  42.     });  
  43. });  

 

 

 

這個例子中我們想從服務器端分別獲取用戶信息和用戶的狀態。 我們使用了一個DeferrredList來等待這兩個請求都完成後進行處理,回調函數先檢查兩個請求是否都成功完成了,如果沒有發生任何錯誤,則遍歷兩個請求分別獲得的用戶數據和狀態數據。 回調函數只有當兩個請求都進入完成狀態後纔會被調用,因此我們不用關心究竟是哪個請求先完成了。 

 

(設想一下,如果沒有DeferredList,你需要如何手動處理這個情形?)

 

 

查看示例

 

結論

 

絕大多數JavaScript應用都需要使用Ajax,因此它們都需要註冊異步的回調函數。dojo.Deferred就提供了這樣一個簡單又優雅的方法。 它可以非常靈活的進行鏈式調用;而使用dojo.DeferredList又可以讓你處理個Deferred對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章