ajax請求的非同步巢狀問題分析

可健康了發表於2014-11-10

(本文章以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模式可以把異部處理用同步的形式表達出來,極大方便了程式碼維護,語意更加清晰,也方便了程式碼複用。

這裡是兩個開源地址: 

as3的promise庫開源下載地址

js的promise開源下載地址(Q.js)

 

嘗試

  將文章最初的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方法,只需一次呼叫即可,是不是很方便呢?

 

 

相關文章