(本文章以as3代碼為例)
問題的產生
在前端開發時,經常會使用到Ajax(Asynchronous Javascript And XML)請求向服務器查詢信息(get)或交換數據(post),ajax請求都是異步響應的,每次請求都不能同步返回結果,而且多次請求嵌套在一起時,邏輯很難處理,怎么辦呢?
在as3中,get請求的寫法通常如下
public static function httpGet(url:String):void { var httpService:HTTPService =new HTTPService(); httpService.url= url; httpService.resultFormat="e4x"; httpService.method = URLRequestMethod.GET; httpService.addEventListener(ResultEvent.RESULT, onSuccess); httpService.addEventListener(FaultEvent.FAULT, onFail); httpService.send(); function onSuccess(result:ResultEvent):void { // do something
} function onFail(fault:FaultEvent):void { // alert error
} }
在ajax請求中,查詢成功的回調函數是異步返回的,無法在HttpGet方法中返回結果,下一步的邏輯處理只能寫在onSuccess方法中,對於查詢結果的邏輯處理都要寫在查詢成功的回調函數內,如果業務邏輯很復雜的話,這種寫法就太麻煩了,而且不能復用。
一種解決思路是通過消息來傳遞查詢結果,當查詢成功或失敗時,發送相應的消息和結果給該次查詢的監聽者,代碼如下(注意紅色加粗部分)
var eventBus:EventDispatcher = new EventDispatcher; public static function httpGetWithMessage(url:String, successMessage:String, failMessage:String):void { var httpService:HTTPService =new HTTPService(); httpService.url= url; httpService.resultFormat="e4x"; httpService.method = URLRequestMethod.GET; httpService.addEventListener(ResultEvent.RESULT, onSuccess); httpService.addEventListener(FaultEvent.FAULT, onFail); httpService.send(); function onSuccess(result:ResultEvent):void { eventBus.dispatchEvent(successMessage, result); } function onFail(fault:FaultEvent):void { eventBus.dispatchEvent(failMessage, fault); } } private function action(url:String):void { var successMSG:String = "success"; var failMSG:String = "fail"; eventBus.addEventListener(successMSG, onSuccess); eventBus.addEventListener(failMSG, onFail); httpGetWithMessage(url, successMSG, failMSG); } private function onSuccess(result:ResultEvent):void { // do something
} private function onFail(fault:FaultEvent):void { // alert error
}
通過消息機制的辦法,可以把查詢成功和失敗的回調函數從action方法中提取出來,從而可以復用這部分代碼,但是,使用消息機制仍然存在4個缺點:
1、必須有一個全局消息總線來控制所有的消息。當查詢次數多時,需要為每次查詢定義不同的消息,還要考慮並發時同一個業務請求的消息不能相同,這種全局消息總線對於消息的管理代價太大;
2、action方法仍然不能復用,每次不同的查詢都需要重新寫一個新的方法;
3、action方法仍然是異部處理,方法本身無法返回查詢結果,以致程序的前后語意不連貫。當請求次數多時,對於一個業務邏輯的處理,必須要分開寫在很多個回調函數內,這些回調函數彼此之間也無法溝通。
4、最重要的一點是,當一個業務邏輯處理需要多次查詢時,每次查詢只能嵌套在上一次查詢的成功回調函數內,如此,除了最內層的查詢可以復用,外內所有的查詢方法都不能復用,代碼極難維護,這時如果需要修改兩個查詢的先后順序,你就慘了。
尋找答案
Promise/Deferred模式 (協議/延時模式)
Promise/Deferred模式最早出現在Javascript的Dojo框架中,它是對異步編程的一種抽象。
- promise處於且只處於三種狀態:未完成,完成,失敗。
- promise的狀態只能從未完成轉化為完成或失敗,不可逆。完成態與失敗態之間不能相互轉化。
- promise的狀態一旦轉化,將不能更改
promise的核心思想可以概括為:把異部處理看作一個協議,無論異部處理的結果是成功還是失敗,都把協議提前返回,等異部處理結束后,協議的接收者自然就知道結果是成功還是失敗了。從形式上說,Promise/Deferred模式可以把異部處理用同步的形式表達出來,極大方便了代碼維護,語意更加清晰,也方便了代碼復用。
這里是兩個開源地址:
嘗試
將文章最初的get請求方法用Promise/Deferred模式改寫一下,首先new一個延時,在發出請求后立即返回,此時這個延時的狀態是“未完成”,當異部請求成功后,回調函數會改變它的狀態為“完成”或“失敗”並傳遞參數,這樣一來,異部邏輯就巧妙的變成了同步邏輯,代碼如下
public static function httpGet(url:String):Promise { var deferred:Deferred = new Deferred(); var httpService:HTTPService =new HTTPService(); httpService.url= url; httpService.resultFormat="e4x"; httpService.method = URLRequestMethod.GET; httpService.addEventListener(ResultEvent.RESULT, onSuccess); httpService.addEventListener(FaultEvent.FAULT, onFail); httpService.send(); return deferred.promise; function onSuccess(result:ResultEvent):void { deferred.resolve(result); } function onFail(fault:FaultEvent):void { deferred.reject(fault); } }
調用時可以這樣寫:
public function process(url:String):void { var p:Promise = httpGet(url); p.then(doSomthing, doError); } public function doSomthing(result:Object):void { } public function doError(result:Object):void { }
最關鍵的一步就是then方法,當請求成功時,執行doSomthing,失敗時執行doError
通過這種方式,異部請求簡化到了腦殘的程度,好處有4點
1、不需要全局消息機制,省了一大陀工作量,且沒有並發問題;
2、請求方法本身與業務邏輯處理完全分開,互不干擾,do something的部分完全可以放在另外的文件中來寫。無論是get請求還是以后的業務邏輯處理方法都是可復用的;
3、請求方法本身直接返回結果,可以同步處理查尋結果。
4、可以鏈式調用、嵌套調用,想怎么用就怎么用~~~
現在假設業務邏輯要實現一個3次查詢的操作,每次查詢URL都依賴上一次的查詢結果,在沒有Promise/Deferred模式之前,只能用3層回調函數嵌套在一直,這簡直是惡夢,不過現在簡單多了,你可以這樣寫:
public function process(url:String):void { var p1:Promise = httpGet(url); p1.then(action_1to2).then(action_2to3).then(action3); function action_1to2(result:Object):Promise { var url2:String = getUrl2(result); var p:Promise = httpGet(url2); return p; } function action_2to3(result:Object):Promise { var url3:String = getUrl3(result); var p:Promise = httpGet(url3); return p; } function action3(result:Object):void { // do something
} }
如上,3個get請求是串行的關系,只需要用then鏈把它們連接起來就可以了,然后自己實現一下getUrl2和getUrl3兩個方法就大功告成了。假如此時需求變了,要求交換一下前兩次查詢的順序,你也只需要改動很少的代碼,爽不爽!個人認為鏈式調用最有用的一點就是邏輯清晰,在視覺上把每一步要做的工作緊密放在一起,一目了然,只要讀這一行代碼就知道第一部做什么,第二步做什么,第三步做什么,維護也方便,比消息機制的回調函數強了無數倍。
最爽的還不只如此,假如3個get請求是並行關系,你還可以這樣寫:
public function process(url1:String, url2:String, url3:String):void { var p1:Promise = httpGet(url1); var p2:Promise = httpGet(url2); var p3:Promise = httpGet(url3); Promise.all([p1, p2, p3]).then(doSomething, doError); } public function doSomething(result:Array):void { var result0:Object = result[0]; var result1:Object = result[1]; var result2:Object = result[2]; // do something
} public function doError(fault:Fault):void { }
當3個請求全部成功時,執行doSomething,只要有一個請求失敗,則執行doError。
假設這時需求又變了,要求在查尋過程中,前端顯示一個loading畫面,查尋結束后,畫面消失,你可以這樣簡單的改一下代碼:
public function process(url1:String, url2:String, url3:String):void { showLoadingImage(); var p1:Promise = httpGet(url1); var p2:Promise = httpGet(url2); var p3:Promise = httpGet(url3); Promise.all([p1, p2, p3]).then(doSomething, doError).always(removeLoadingImage); } function doSomething(result:Array):void { var result0:Object = result[0]; var result1:Object = result[1]; var result2:Object = result[2]; // do something
} function doError(fault:Fault):void { }
always方法的含意是無論前面的協議成功或者失敗,都執行下一個方法。在Promise/Deferred模式的情況下,你不用在3次請求的6個回調函數里分別來執行removeLoadingImage方法,只需一次調用即可,是不是很方便呢?