iOS如何優雅的處理“回調地獄Callback hell”(二)——使用Swift

前言

在上篇中,我談到了可以用promise來解決Callback hell的問題,這篇我們換一種方式一樣可以解決這個問題。

我們先分析一下為何promise能解決多層回調嵌套的問題,經過上篇的分析,我總結也一下幾點:

1.promise封裝了所有異步操作,把異步操作封裝成了一個“盒子”。
2.promise提供了Monad,then相當于flatMap。
3.promise的函數返回對象本身,于是就可形成鏈式調用

好了,既然這些能優雅的解決callback hell,那么我們只要能做到這些,也一樣可以完成任務。到這里大家可能就已經恍然大悟了,Swift就是完成這個任務的最佳語言!Swift支持函數式編程,分分鐘就可以完成promise的基本功能。

一.利用Swift特性處理回調Callback hell

我們還是以上篇的例子來舉例,先來描述一下場景:
假設有這樣一個提交按鈕,當你點擊之后,就會提交一次任務。當你點下按鈕的那一刻,首先要先判斷是否有權限提交,沒有權限就彈出錯誤。有權限提交之后,還要請求一次,判斷當前任務是否已經存在,如果存在,彈出錯誤。如果不存在,這個時候就可以安心提交任務了。

那么代碼如下:

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)
        }
    )
}

接下來我們就來優雅的解決上述看上去不好維護的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,以方便后面可以繼續鏈式調用。

// 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對應的其實就是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思想的精髓,優雅的完美的處理了回調地獄。這也得益于Swift語言的優點。

文章至此,雖然已經解決了問題了,不過還沒有結束,我們還可以繼續再進一步討論一些東西。

二.進一步的討論

1.@noescape,throws,rethrows關鍵字
flatMap還有這種寫法:

func flatMap<U> (@noescape f: T throws -> Async<U>)rethrows -> Async<U> 

@noescape 從字面上看,就知道是“不會逃走”的意思,這個關鍵字專門用于修飾函數閉包這種參數類型的,當出現這個參數時,它表示該閉包不會跳出這個函數調用的生命期:即函數調用完之后,這個閉包的生命期也結束了。
在蘋果官方文檔上是這樣寫的:

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.

那什么時候一個閉包參數會跳出函數的生命期呢?

引用唐巧大神的解釋:

在函數實現內,將一個閉包用 dispatch_async
嵌套,這樣這個閉包就會在另外一個線程中存在,從而跳出了當前函數的生命期。這樣做主要是可以幫助編譯器做性能的優化。

throws關鍵字是代表該閉包可能會拋出異常。
rethrows關鍵字是代表這個閉包如果拋出異常,僅可能是因為傳遞給它的閉包的調用導致了異常。

2.繼續說說上面例子里面的Result,和Async一樣,我們也可以繼續封裝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.上面我們已經把Async和Result封裝了map方法,所以他們也可以叫做函子(Functor)。接下來可以繼續封裝,把他們都封裝成適用函子(Applicative Functor)單子(Monad)

適用函子(Applicative Functor)根據定義:
對于任意一個函子F,如果能支持以下運算,該函子就是一個適用函子:

func pure<A>(value:A) ->F<A>

func <*><A,B>(f:F<A - > B>, x:F<A>) ->F<B>

以Async為例,我們為它加上這兩個方法


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)根據定義:
對于任意一個類型構造體F定義了下面兩個函數,它就是一個單子Monad:

func pure<A>(value:A) ->F<A>

func flatMap<A,B>(x:F<A>)->(A->F<B>)->F<B>

還是以Async為例,此時的Async已經有了unit和flatMap滿足定義了,這個時候,就可以說Async已經是一個Monad了。

至此,我們就把Async和Result都變成了適用函子(Applicative Functor)單子(Monad)了。

4.再說說運算符。
flatMap函數有時候會被定義為一個運算符>>=。由于它會將第一個參數的計算結果綁定到第二個參數的輸入上面,這個運算符也會被稱為“綁定(bind)”運算.

為了方便,那我們就把上面的4個操作都定義成運算符吧。

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)
}

按照順序,第二個對應的就是原來的map函數,第三個對應的就是原來的flatMap函數。

5.說到運算符,我們這里還可以繼續回到文章最開始的地方去討論一下那段回調地獄的代碼。上面我們通過map和flatMap成功的展開了Callback hell,其實這里還有另外一個方法可以解決問題,那就是用自定義運算符。這里我們用不到適用函子的<*>,有些問題就可能用到它。還是回到上述問題,這里我們用Monad里面的運算符來解決回調地獄。


func requestAsyncOperation(request : String ) -> Async <String>
{
    return fetchDataAPI(request) >>= (fetchOtherDataAPI) <^>(transformResult) >>= (fetchOtherDataAPI) <^> (transformTaskResult)
}

通過運算符,最終原來的40多行代碼變成了最后一行了!當然,我們中間封裝了一些操作。

三.總結

經過上篇和本篇的討論,優雅的處理"回調地獄Callback hell"的方法有以下幾種:
1.使用PromiseKit
2.使用Swift的map和flatMap封裝異步操作(思想和promise差不多)
3.使用Swift自定義運算符展開回調嵌套

目前為止,我能想到的處理方法還有2種:
4.使用Reactive cocoa
5.使用RxSwift

下篇或者下下篇可能應該就是討論RAC和RxSwift如果優雅的處理回調地獄了。如果大家還有什么其他方法能優雅的解決這個問題,也歡迎大家提出來,一起討論,相互學習!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容