前言
在上篇中,我談到了可以用promise來解決Callback hell的問題犬辰,這篇我們換一種方式一樣可以解決這個問題浑吟。
我們先分析一下為何promise能解決多層回調(diào)嵌套的問題,經(jīng)過上篇的分析赶盔,我總結(jié)也一下幾點(diǎn):
1.promise封裝了所有異步操作历恐,把異步操作封裝成了一個“盒子”。
2.promise提供了Monad专筷,then相當(dāng)于flatMap弱贼。
3.promise的函數(shù)返回對象本身,于是就可形成鏈?zhǔn)秸{(diào)用
好了磷蛹,既然這些能優(yōu)雅的解決callback hell吮旅,那么我們只要能做到這些,也一樣可以完成任務(wù)味咳。到這里大家可能就已經(jīng)恍然大悟了庇勃,Swift就是完成這個任務(wù)的最佳語言!Swift支持函數(shù)式編程槽驶,分分鐘就可以完成promise的基本功能责嚷。
一.利用Swift特性處理回調(diào)Callback hell
我們還是以上篇的例子來舉例,先來描述一下場景:
假設(shè)有這樣一個提交按鈕掂铐,當(dāng)你點(diǎn)擊之后罕拂,就會提交一次任務(wù)。當(dāng)你點(diǎn)下按鈕的那一刻全陨,首先要先判斷是否有權(quán)限提交爆班,沒有權(quán)限就彈出錯誤。有權(quán)限提交之后辱姨,還要請求一次柿菩,判斷當(dāng)前任務(wù)是否已經(jīng)存在,如果存在雨涛,彈出錯誤枢舶。如果不存在懦胞,這個時候就可以安心提交任務(wù)了。
那么代碼如下:
func requestAsyncOperation(request : String , success : String -> Void , failure : NSError -> Void)
{
WebRequestAPI.fetchDataAPI(request, success : { result in
WebOtherRequestAPI.fetchOtherDataAPI ( result , success : {OtherResult in
[self fulfillData:OtherResult];
let finallyTheParams = self.transformResult(OtherResult)
TaskAPI.fetchOtherDataAPI ( finallyTheParams , success : { TaskResult in
let finallyTaskResult = self.transformTaskResult(TaskResult)
success(finallyTaskResult)
},
failure:{ TaskError in
failure(TaskError)
}
)
},failure : { ExistError in
failure(ExistError)
}
)
} , failure : { AuthorityError in
failure(AuthorityError)
}
)
}
接下來我們就來優(yōu)雅的解決上述看上去不好維護(hù)的Callback hell祟辟。
1.首先我們要封裝異步操作医瘫,把異步操作封裝到Async中,順帶把返回值也一起封裝成Result旧困。
enum Result <T> {
case Success(T)
case Failure(ErrorType)
}
struct Async<T> {
let trunk:(Result<T>->Void)->Void
init(function:(Result<T>->Void)->Void) {
trunk = function
}
func execute(callBack:Result<T>->Void) {
trunk(callBack)
}
}
2.封裝Monad醇份,提供Map和flatMap操作。順帶返回值也返回Async吼具,以方便后面可以繼續(xù)鏈?zhǔn)秸{(diào)用僚纷。
// Monad
extension Async{
func map<U>(f: T throws-> U) -> Async<U> {
return flatMap{ .unit(try f($0)) }
}
func flatMap<U>(f:T throws-> Async<U>) -> Async<U> {
return Async<U>{ cont in
self.execute{
switch $0.map(f){
case .Success(let async):
async.execute(cont)
case .Failure(let error):
cont(.Failure(error))
}
}
}
}
}
這是我們把異步的過程就封裝成一個盒子了,盒子里面有Map拗盒,flatMap操作怖竭,flatMap對應(yīng)的其實就是promise的then
3.我們可以把flatMap名字直接換成then,那么之前那30多行的代碼就會簡化成下面這樣:
func requestAsyncOperation(request : String ) -> Async <String>
{
return fetchDataAPI(request)
.then(fetchOtherDataAPI)
.map(transformResult)
.then(fetchOtherDataAPI)
.map(transformTaskResult)
}
基本上和用promise一樣的效果陡蝇。這樣就不用PromiseKit庫痊臭,利用promise思想的精髓,優(yōu)雅的完美的處理了回調(diào)地獄登夫。這也得益于Swift語言的優(yōu)點(diǎn)广匙。
文章至此,雖然已經(jīng)解決了問題了恼策,不過還沒有結(jié)束鸦致,我們還可以繼續(xù)再進(jìn)一步討論一些東西。
二.進(jìn)一步的討論
1.@noescape涣楷,throws分唾,rethrows關(guān)鍵字
flatMap還有這種寫法:
func flatMap<U> (@noescape f: T throws -> Async<U>)rethrows -> Async<U>
@noescape 從字面上看,就知道是“不會逃走”的意思狮斗,這個關(guān)鍵字專門用于修飾函數(shù)閉包這種參數(shù)類型的绽乔,當(dāng)出現(xiàn)這個參數(shù)時,它表示該閉包不會跳出這個函數(shù)調(diào)用的生命期:即函數(shù)調(diào)用完之后碳褒,這個閉包的生命期也結(jié)束了迄汛。
在蘋果官方文檔上是這樣寫的:
A new @noescape attribute may be used on closure parameters to functions. This indicates that the parameter is only ever called (or passed as an @noescape parameter in a call), which means that it cannot outlive the lifetime of the call. This enables some minor performance optimizations, but more importantly disables the self. requirement in closure arguments.
那什么時候一個閉包參數(shù)會跳出函數(shù)的生命期呢?
引用唐巧大神的解釋:
在函數(shù)實現(xiàn)內(nèi)骤视,將一個閉包用 dispatch_async
嵌套鞍爱,這樣這個閉包就會在另外一個線程中存在,從而跳出了當(dāng)前函數(shù)的生命期专酗。這樣做主要是可以幫助編譯器做性能的優(yōu)化睹逃。
throws關(guān)鍵字是代表該閉包可能會拋出異常。
rethrows關(guān)鍵字是代表這個閉包如果拋出異常,僅可能是因為傳遞給它的閉包的調(diào)用導(dǎo)致了異常沉填。
2.繼續(xù)說說上面例子里面的Result疗隶,和Async一樣,我們也可以繼續(xù)封裝Result翼闹,也加上map和flatMap方法斑鼻。
func ==<T:Equatable>(lhs:Result<T>, rhs:Result<T>) -> Bool{
if case (.Success(let l), .Success(let r)) = (lhs, rhs){
return l == r
}
return false
}
extension Result{
func map<U>(f:T throws-> U) -> Result<U> {
return flatMap{.unit(try f($0))}
}
func flatMap<U>(f:T throws-> Result<U>) -> Result<U> {
switch self{
case .Success(let value):
do{
return try f(value)
}catch let e{
return .Failure(e)
}
case .Failure(let e):
return .Failure(e)
}
}
}
3.上面我們已經(jīng)把Async和Result封裝了map方法,所以他們也可以叫做函子(Functor)猎荠。接下來可以繼續(xù)封裝坚弱,把他們都封裝成適用函子(Applicative Functor)和單子(Monad)
適用函子(Applicative Functor)根據(jù)定義:
對于任意一個函子F,如果能支持以下運(yùn)算关摇,該函子就是一個適用函子:
func pure<A>(value:A) ->F<A>
func <*><A,B>(f:F<A - > B>, x:F<A>) ->F<B>
以Async為例荒叶,我們?yōu)樗由线@兩個方法
extension Async{
static func unit(x:T) -> Async<T> {
return Async{ $0(.Success(x)) }
}
func map<U>(f: T throws-> U) -> Async<U> {
return flatMap{ .unit(try f($0)) }
}
func flatMap<U>(f:T throws-> Async<U>) -> Async<U> {
return Async<U>{ cont in
self.execute{
switch $0.map(f){
case .Success(let async):
async.execute(cont)
case .Failure(let error):
cont(.Failure(error))
}
}
}
}
func apply<U>(af:Async<T throws-> U>) -> Async<U> {
return af.flatMap(map)
}
}
unit和apply就是上面定義中的兩個方法。接下來我們在看看Monad的定義输虱。
單子(Monad)根據(jù)定義:
對于任意一個類型構(gòu)造體F定義了下面兩個函數(shù)些楣,它就是一個單子Monad:
func pure<A>(value:A) ->F<A>
func flatMap<A,B>(x:F<A>)->(A->F<B>)->F<B>
還是以Async為例,此時的Async已經(jīng)有了unit和flatMap滿足定義了宪睹,這個時候愁茁,就可以說Async已經(jīng)是一個Monad了。
至此亭病,我們就把Async和Result都變成了適用函子(Applicative Functor)和單子(Monad)了鹅很。
4.再說說運(yùn)算符。
flatMap函數(shù)有時候會被定義為一個運(yùn)算符>>=命贴。由于它會將第一個參數(shù)的計算結(jié)果綁定到第二個參數(shù)的輸入上面道宅,這個運(yùn)算符也會被稱為“綁定(bind)”運(yùn)算.
為了方便食听,那我們就把上面的4個操作都定義成運(yùn)算符吧胸蛛。
func unit<T> (x:T) -> Async<T> {
return Async{$0(.Success(x))}
}
func <^> <T, U> (f: T throws-> U, async: Async<T>) -> Async<U> {
return async.map(f)
}
func >>= <T, U> (async:Async<T>, f:T throws-> Async<U>) -> Async<U> {
return async.flatMap(f)
}
func <*> <T, U> (af: Async<T throws-> U>, async:Async<T>) -> Async<U> {
return async.apply(af)
}
按照順序,第二個對應(yīng)的就是原來的map函數(shù)樱报,第三個對應(yīng)的就是原來的flatMap函數(shù)葬项。
5.說到運(yùn)算符,我們這里還可以繼續(xù)回到文章最開始的地方去討論一下那段回調(diào)地獄的代碼迹蛤。上面我們通過map和flatMap成功的展開了Callback hell民珍,其實這里還有另外一個方法可以解決問題,那就是用自定義運(yùn)算符盗飒。這里我們用不到適用函子的<*>嚷量,有些問題就可能用到它。還是回到上述問題逆趣,這里我們用Monad里面的運(yùn)算符來解決回調(diào)地獄蝶溶。
func requestAsyncOperation(request : String ) -> Async <String>
{
return fetchDataAPI(request) >>= (fetchOtherDataAPI) <^>(transformResult) >>= (fetchOtherDataAPI) <^> (transformTaskResult)
}
通過運(yùn)算符,最終原來的40多行代碼變成了最后一行了!當(dāng)然抖所,我們中間封裝了一些操作梨州。
三.總結(jié)
經(jīng)過上篇和本篇的討論,優(yōu)雅的處理"回調(diào)地獄Callback hell"的方法有以下幾種:
1.使用PromiseKit
2.使用Swift的map和flatMap封裝異步操作(思想和promise差不多)
3.使用Swift自定義運(yùn)算符展開回調(diào)嵌套
目前為止田轧,我能想到的處理方法還有2種:
4.使用Reactive cocoa
5.使用RxSwift
下篇或者下下篇可能應(yīng)該就是討論RAC和RxSwift如果優(yōu)雅的處理回調(diào)地獄了暴匠。如果大家還有什么其他方法能優(yōu)雅的解決這個問題,也歡迎大家提出來傻粘,一起討論每窖,相互學(xué)習(xí)!