在上篇中,我談到了可以用promise來解決Callback hell的問題,這篇我們換一種方式一樣可以解決這個問題。
我們先分析一下為何promise能解決多層回撥巢狀的問題,經過上篇的分析,我總結也一下幾點:
1.promise封裝了所有非同步操作,把非同步操作封裝成了一個“盒子”。
2.promise提供了Monad,then相當於flatMap。
3.promise的函式返回物件本身,於是就可形成鏈式呼叫
好了,既然這些能優雅的解決callback hell,那麼我們只要能做到這些,也一樣可以完成任務。到這裡大家可能就已經恍然大悟了,Swift就是完成這個任務的最佳語言!Swift支援函數語言程式設計,分分鐘就可以完成promise的基本功能。
一.利用Swift特性處理回撥Callback hell
我們還是以上篇的例子來舉例,先來描述一下場景:
假設有這樣一個提交按鈕,當你點選之後,就會提交一次任務。當你點下按鈕的那一刻,首先要先判斷是否有許可權提交,沒有許可權就彈出錯誤。有許可權提交之後,還要請求一次,判斷當前任務是否已經存在,如果存在,彈出錯誤。如果不存在,這個時候就可以安心提交任務了。
那麼程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
enum Result { case Success(T) case Failure(ErrorType) } struct Async { let trunk:(Result->Void)->Void init(function:(Result->Void)->Void) { trunk = function } func execute(callBack:Result->Void) { trunk(callBack) } } |
2.封裝Monad,提供Map和flatMap操作。順帶返回值也返回Async,以方便後面可以繼續鏈式呼叫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Monad extension Async{ func map(f: T throws-> U) -> Async { return flatMap{ .unit(try f($0)) } } func flatMap(f:T throws-> Async) -> Async { return Async{ 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多行的程式碼就會簡化成下面這樣:
1 2 3 4 5 6 7 8 |
func requestAsyncOperation(request : String ) -> Async { return fetchDataAPI(request) .then(fetchOtherDataAPI) .map(transformResult) .then(fetchOtherDataAPI) .map(transformTaskResult) } |
基本上和用promise一樣的效果。這樣就不用PromiseKit庫,利用promise思想的精髓,優雅的完美的處理了回撥地獄。這也得益於Swift語言的優點。
文章至此,雖然已經解決了問題了,不過還沒有結束,我們還可以繼續再進一步討論一些東西。
二.進一步的討論
1.@noescape,throws,rethrows關鍵字
flatMap還有這種寫法:
1 |
func flatMap (@noescape f: T throws -> Async)rethrows -> Async |
@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方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
func ==(lhs:Result, rhs:Result) -> Bool{ if case (.Success(let l), .Success(let r)) = (lhs, rhs){ return l == r } return false } extension Result{ func map(f:T throws-> U) -> Result { return flatMap{.unit(try f($0))} } func flatMap(f:T throws-> Result) -> Result { 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,如果能支援以下運算,該函子就是一個適用函子:
1 2 3 |
func pure(value:A) ->F func (f:F B>, x:F) ->F |
以Async為例,我們為它加上這兩個方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
extension Async{ static func unit(x:T) -> Async { return Async{ $0(.Success(x)) } } func map(f: T throws-> U) -> Async { return flatMap{ .unit(try f($0)) } } func flatMap(f:T throws-> Async) -> Async { return Async{ cont in self.execute{ switch $0.map(f){ case .Success(let async): async.execute(cont) case .Failure(let error): cont(.Failure(error)) } } } } func apply(af:Async U>) -> Async { return af.flatMap(map) } } |
unit和apply就是上面定義中的兩個方法。接下來我們在看看Monad的定義。
單子(Monad)根據定義:
對於任意一個型別構造體F定義了下面兩個函式,它就是一個單子Monad:
1 2 3 |
func pure(value:A) ->F func flatMap(x:F)->(A->F)->F |
還是以Async為例,此時的Async已經有了unit和flatMap滿足定義了,這個時候,就可以說Async已經是一個Monad了。
至此,我們就把Async和Result都變成了適用函子(Applicative Functor)和單子(Monad)了。
4.再說說運算子。
flatMap函式有時候會被定義為一個運算子>>=。由於它會將第一個引數的計算結果繫結到第二個引數的輸入上面,這個運算子也會被稱為“繫結(bind)”運算.
為了方便,那我們就把上面的4個操作都定義成運算子吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func unit (x:T) -> Async { return Async{$0(.Success(x))} } func (f: T throws-> U, async: Async) -> Async { return async.map(f) } func >>= (async:Async, f:T throws-> Async) -> Async { return async.flatMap(f) } func (af: Async U>, async:Async) -> Async { return async.apply(af) } |
按照順序,第二個對應的就是原來的map函式,第三個對應的就是原來的flatMap函式。
5.說到運算子,我們這裡還可以繼續回到文章最開始的地方去討論一下那段回撥地獄的程式碼。上面我們通過map和flatMap成功的展開了Callback hell,其實這裡還有另外一個方法可以解決問題,那就是用自定義運算子。這裡我們用不到適用函子的,有些問題就可能用到它。還是回到上述問題,這裡我們用Monad裡面的運算子來解決回撥地獄。
1 2 3 4 |
func requestAsyncOperation(request : String ) -> Async { 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如果優雅的處理回撥地獄了。如果大家還有什麼其他方法能優雅的解決這個問題,也歡迎大家提出來,一起討論,相互學習!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式