談一談Promise

pan-git-hub發表於2019-02-16

本文將會向你簡單介紹promise,並在最後嘗試使用OC實現一個可用的promise庫。

什麼是promise?

在物件導向的世界promise也並不特殊。promise物件表示非同步操作的最終完成(或失敗)及其結果值。

這樣一句話顯然無法讓人理解promise,它是做什麼的?要怎麼使用它?為什麼要使用它?下面的內容將會解釋這些問題。即使現在完全不理解promise是什麼,請先記住“promise”這個字面含義給你帶來的暗示:某件事情(程式碼)在未來的某個時刻發生(執行)。

我們遇到的問題

通常我們使用block回撥來處理一些非同步操作,比如:

[object doSomethingWithArg:arg handler:^(id data){  
    //resolve data
}];
複製程式碼

上面的程式碼沒有問題,然而假設我們遇到了如下場景:發起網路請求A>獲取網路請求A的返回資料A>使用返回資料A作為引數發起網路請求B…如此往復,通常還包含著網路錯誤的回撥,程式碼看起來是這樣的:

//ViewController.m
- (void)viewDidLoad {
    [[XXNetwork shared] requestAWithArg:arg success:^(id dataA){  
        [[XXNetwork shared] requestBWithArg:dataA success:^(id dataB){  
            [[XXNetwork shared] requestCWithArg:dataB success:^(id dataC){  
                    //resolve dataC
            } failure:^(NSError *error){
                //網路錯誤處理
            }];
        } failure:^(NSError *error){
            //網路錯誤處理
        }];
    } failure:^(NSError *error){
        //網路錯誤處理
    }];
}

//XXNetwork.h
//非同步的網路請求
- (void)requestAWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure;
- (void)requestBWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure;
- (void)requestCWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure;


//XXNetwork.m
//原有的方法
- (void)requestAWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure {
	//doSomething
}
...
複製程式碼

上面程式碼看起來還算友好,但是當這樣的巢狀太深的時候,問題就出現了:

1.過多的巢狀造成程式碼無法被輕鬆的閱讀

2.網路錯誤的狀況沒有統一的處理

這樣場景下使用block回撥巢狀顯然不夠優雅,那麼我們要怎麼做?

使用promise改造程式碼

對於我們遇到的問題,promise將會大顯身手,使用promise改造後程式碼看起來大概是這樣的:

//ViewController.m
- (void)viewDidLoad {
    [[XXNetwork shared] requestAWithArg:arg].then(^id(id value){
        //value 即 dataA
        return [[XXNetwork shared] requestBWithArg:value];
    }).then(^id(id value){
        //value 即 dataB
        return [[XXNetwork shared] requestCWithArg:value];
    }).then(^id(id value){
        //value 即 dataC
        //resolve dataC
    }).catch(^id(NSError * error){
        //網路錯誤處理
    });
}



//XXNetwork.h
//原有的方法
//非同步的網路請求
- (void)requestAWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure;
- (void)requestBWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure;
- (void)requestCWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure;


//包裝後的方法
- (Promise *)requestAWithArg:(id)arg;
- (Promise *)requestBWithArg:(id)arg;
- (Promise *)requestCWithArg:(id)arg;



//XXNetwork.m
//原有的方法
- (void)requestAWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure {
	//doSomething
}
...
//包裝後的方法
- (Promise *)requestAWithArg:(id)arg {
    Promise *p = [Promise new];
    [[XXNetwork shared] requestAWithArg:arg success:^(id dataA){
        p.fulfill(dataA);
    } failure:^(NSError *error){
    	p.reject(error);
    }];
    return p;
}
...
複製程式碼

viewDidLoad中的程式碼表示的是:發起網路請求A,然後(then)使用返回資料A作為引數發起網路請求B,然後(then)使用返回資料B作為引數發起網路請求C,然後(then)處理dataC。catch則會處理鏈上產生的錯誤。你會發現,這段程式碼閱讀下來非常貼近日常的語言習慣,可怕的回撥地獄不見了,網路錯誤有地方做統一處理,太酷了對不對?更重要的是我們只需要做一些簡單的改造或包裝。

promise是怎麼運作的

在感嘆promise的優雅之後,我們產生這些疑問:then是什麼?catch是什麼?value從哪裡來的?為什麼需要返回一個promise物件?promise物件的fulfill和reject方法是幹什麼的?

為了方便理解,先將viewDidLoad中的程式碼縮減一部分,其他不變,然後看一下執行過程

//ViewController.m
- (void)viewDidLoad {
    [[XXNetwork shared] requestAWithArg:arg].then(^id(id value){
        //value 即 dataA
    });
}

//XXNetwork.h
//原有的方法
//非同步的網路請求
- (void)requestAWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure;
- (void)requestBWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure;
- (void)requestCWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure;


//包裝後的方法
- (Promise *)requestAWithArg:(id)arg;
- (Promise *)requestBWithArg:(id)arg;
- (Promise *)requestCWithArg:(id)arg;



//XXNetwork.m
//原有的方法
- (void)requestAWithArg:(id)arg success:(void (^)(id data))success failure:(void (^)(NSError *error))failure {
	//doSomething
}
...
//包裝後的方法
- (Promise *)requestAWithArg:(id)arg {
    Promise *p = [Promise new];
    [[XXNetwork shared] requestAWithArg:arg success:^(id dataA){
        p.fulfill(dataA);
    } failure:^(NSError *error){
    	p.reject(error);
    }];
    return p;
}
...
複製程式碼

1.在viewDidLoad執行後,執行XXNetwork單例requestAWithArg:方法

2.在requestAWithArg:方法內建立了一個promise物件。呼叫原有的方法requestAWithArg:success:failure:,如果非同步請求成功則將會呼叫promise的fulfill方法,失敗則將會呼叫reject方法。返回這個promise物件

3.呼叫promise的then方法,將成功後的需要執行的block加入到promise中

4.最後當requestAWithArg:success:failure:進入成功回撥則呼叫promise的fulfill方法,使用promise的then方法加入的block會執行。同理當requestAWithArg:success:failure:進入失敗回撥則呼叫promise的reject方法,使用promise的catch方法加入的block就會執行

回想一開始我們對promise物件的描述:promise物件表示非同步操作的最終完成(或失敗)及其結果值。現在腦海中有一些輪廓正在出現,讓我們結合下面圖將它梳理清晰:

談一談Promise

promise物件始終處於以下3個狀態之一:

  • pending初始化狀態

  • fulfilled表示操作已經完成

  • rejected表示操作已經失敗

當我們建立一個promise物件時,promise處於pending狀態;我們可以通過promise的then,catch等方法將成功或失敗後需要執行的任務(block)加入到promise的“回撥列表”;當非同步操作完成後呼叫promise的fulfill或reject方法,並傳遞引數;promise的狀態從pending轉換到fulfilled或rejected,這樣的轉換是不可逆的,同時會呼叫之前使用的then或者catch加入到該promise任務(block);then或者catch方法將會返回一個新的promise物件,我們可以對新的promise繼續呼叫then或catch方法,從而形成了鏈式呼叫。這就是promise的核心部分。

目前為止僅簡單介紹了promise的一部分,不再做更多的使用介紹,通過以下網址可以獲取更多的關於promise的規範和使用方法

developer.mozilla.org/en-US/docs/…

promisesaplus.com/

在程式碼中使用promise之前

Promise並非OC原生提供,使用PromiseKit是一個好的選擇,它有豐富可靠的API,你可以在這裡找到它,具體的使用方法參考其文件。

使用promise讓我們遠離了回撥地獄,但是我們可以思考下一些問題要如何應對:失去了引數的型別資訊要如何處理?promise是否有效能問題?能否中止一個promise的鏈?引入promise的學習成本有多少等等。這些會問題可以在實踐中解開,找到適合使用的場景,做好權衡。

實現一個可用promise庫

在瞭解一些規範後我們可以嘗試自己實現一個可用的庫ToyPromise,你會看到then,catch,finally,race,all這些熟悉的API的具體實現。由於一些原因使用上和promise的規範有一些差異,但核心的部分是不變的。ToyPromise目前僅是個玩具,歡迎貢獻程式碼讓它成長。

相關文章