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阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容