本文將會向你簡單介紹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物件始終處於以下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/…
在程式碼中使用promise之前
Promise並非OC原生提供,使用PromiseKit是一個好的選擇,它有豐富可靠的API,你可以在這裡找到它,具體的使用方法參考其文件。
使用promise讓我們遠離了回撥地獄,但是我們可以思考下一些問題要如何應對:失去了引數的型別資訊要如何處理?promise是否有效能問題?能否中止一個promise的鏈?引入promise的學習成本有多少等等。這些會問題可以在實踐中解開,找到適合使用的場景,做好權衡。
實現一個可用promise庫
在瞭解一些規範後我們可以嘗試自己實現一個可用的庫ToyPromise,你會看到then,catch,finally,race,all這些熟悉的API的具體實現。由於一些原因使用上和promise的規範有一些差異,但核心的部分是不變的。ToyPromise目前僅是個玩具,歡迎貢獻程式碼讓它成長。