优雅的PromiseKit

背景

之前就了解到js中有Promise这么一个东西,可以很友好的实现异步方法,后来偶然在一段ios开源代码中看到这么一段用法:

firstly {
    login()
}.then { creds in
    fetch(avatar: creds.user)
}.done { image in
    self.imageView = image
}

眼前一亮,firstly第一步做xxx,then接下来做xxx,done完成了之后最后做xxx,这个写法真是太swift了,顿时产生了兴趣。
虽然实现异步回调我也有ReactCocoa的方案,但其中不乏一些晦涩难懂的知识需要理解,例如冷信号与热信号,最让人吐槽的还是它的语法,写一个简单的逻辑就需要new各种Producer,切线程调用的方法又老是分不清subscribeOn和observeOn,而且放的位置不同还影响执行顺序。
总之,在看到Promise语法之后,世界变得美好多了,接下来我们就进入Promise的世界吧。

PromiseKit

then & done

Promise对象就是一个ReactCocoa中的SignalProducer,它可以异步fullfill返回一个成功对象或者reject返回一个错误信号。

Promise { sink in
    it.requestJson().on(failed: { err in
        sink.reject(err)
    }, value: { data in
        sink.fulfill(data)
    }).start()
}

接下来就是把它用在各个方法块里面了,例如:

firstly {
    Promise { sink in
        indicator.show(inView: view, text: text, detailText: nil, animated: true)
        sink.fulfill()
    }
}.then {
        api.promise(format: .json)
}.ensure {
        indicator.hide(inView: view, animated: true)
}.done { data in
        let params = data.result!["args"] as! [String: String]
        assert((Constant.baseParams + Constant.params) == params)
}.catch { error in
        assertionFailure()
}
        

firstly是可选的,它只能放在第一个,是为了代码能更加的优雅和整齐,他的block里也是return一个Promise。
then是接在中间的,可以无限多个then相互连接,顾名思义,就像我们讲故事可以不断地有然后、然后、然后...then也是要求返回一个Promise对象的,也就是说,任何一个then都可以抛出一个error,中断事件。
ensure类似于finally,不管事件是否错误,它都一定会得到执行,ensure不同于finally的是,它可以放在任何位置。
done是事件结束的标志,它是必须要有的,只有上面的事件都执行成功时,才会最终执行done。
catch是捕获异常,done之前的任何事件出现错误,都会直接进入catch。

上面代码的含义就是先显示loading,然后请求api,不管api是否请求成功,都要确保loading隐藏,然后如果成功,则打印数据,否则打印异常。

Guarantee

Guarantee是Promise的特殊情况,当我们确保事件不会有错误的时候,就可以用Guarantee来代替Promise,有它就不需要catch来捕获异常了:

firstly {
    after(seconds: 0.1)
}.done {
    // there is no way to add a `catch` because after cannot fail.
}

after是一个延迟执行的方法,它就返回了一个Guarantee对象,因为延迟执行是一定不会失败的,所以我们只需要后续接done就行了。

map

map是指一次数据的变换,而不是一次事件,例如我们要把从接口返回的json数据转换成对象,就可以用map,map返回的也是一个对象,而不是Promise。

tap

tap是一个无侵入的事件,类似于Reactivecocoa的doNext,他不会影响事件的任何属性,只是在适当的时机做一些不影响主线的事情,适用于打点:

firstly {
    foo()
}.tap {
    print($0)
}.done {
    //…
}.catch {
    //…
}

when

when是个可以并行执行多个任务的好东西,when中当所有事件都执行完成,或者有任何一个事件执行失败,都会让事件进入下一阶段,when还有一个concurrently属性,可以控制并发执行任务的最多数量:

firstly {
    Promise { sink in
        indicator.show(inView: view, text: text, detailText: nil, animated: true)
        sink.fulfill()
    }
}.then {
        when(fulfilled: api.promise(format: .json), api2.promise(format: .json))
}.ensure {
        indicator.hide(inView: view, animated: true)
}.done { data, data2 in
        assertionFailure()
        expectation.fulfill()
}.catch { error in
        assert((error as! APError).description == err.description)
        expectation.fulfill()
}

这个方法还是很常用的,当我们要同时等2,3个接口的数据都拿到,再做后续的事情的时候,就适合用when了。

on

PromiseKit的切换线程非常的方便和直观,只需要在方法中传入on的线程即可:

firstly {
    user()
}.then(on: DispatchQueue.global()) { user in
    URLSession.shared.dataTask(.promise, with: user.imageUrl)
}.compactMap(on: DispatchQueue.global()) {
    UIImage(data: $0)
}

哪个方法需要指定线程就在那个方法的on传入对应的线程。

throw

如果then中需要抛出异常,一种方法是在Promise中调用reject,另一种比较简便的方法就是直接throw:

firstly {
    foo()
}.then { baz in
    bar(baz)
}.then { result in
    guard !result.isBad else { throw MyError.myIssue }
    //…
    return doOtherThing()
}

如果调用的方法可能会抛出异常,try也会让异常直达catch:

foo().then { baz in
    bar(baz)
}.then { result in
    try doOtherThing()
}.catch { error in
    // if doOtherThing() throws, we end up here
}

recover

CLLocationManager.requestLocation().recover { error -> Promise<CLLocation> in
    guard error == MyError.airplaneMode else {
        throw error
    }
    return .value(CLLocation.savannah)
}.done { location in
    //…
}

recover能从异常中拯救任务,可以判定某些错误就忽略,当做正常结果返回,剩下的错误继续抛出异常。

几个例子

列表每行顺序依次渐变消失

let fade = Guarantee()
for cell in tableView.visibleCells {
    fade = fade.then {
        UIView.animate(.promise, duration: 0.1) {
            cell.alpha = 0
        }
    }
}
fade.done {
    // finish
}

执行一个方法,指定超时时间

let fetches: [Promise<T>] = makeFetches()
let timeout = after(seconds: 4)

race(when(fulfilled: fetches).asVoid(), timeout).then {
    //…
}

race和when不一样,when会等待所有任务执行成功再继续,race是谁第一个到就继续,race要求所有任务返回类型必须一样,最好的做法是都返回Void,上面的例子就是让4秒计时和请求api同时发起,如果4秒计时到了请求还没回来,则直接调用后续方法。

至少等待一段时间做某件事

let waitAtLeast = after(seconds: 0.3)

firstly {
    foo()
}.then {
    waitAtLeast
}.done {
    //…
}

上面的例子从firstly中的foo执行之前就已经开始after(seconds: 0.3),所以如果foo执行超过0.3秒,则foo执行完后不会再等待0.3秒,而是直接继续下一个任务。如果foo执行不到0.3秒,则会等待到0.3秒再继续。这个方法的场景可以用在启动页动画,动画显示需要一个保证时间。

重试

func attempt<T>(maximumRetryCount: Int = 3, delayBeforeRetry: DispatchTimeInterval = .seconds(2), _ body: @escaping () -> Promise<T>) -> Promise<T> {
    var attempts = 0
    func attempt() -> Promise<T> {
        attempts += 1
        return body().recover { error -> Promise<T> in
            guard attempts < maximumRetryCount else { throw error }
            return after(delayBeforeRetry).then(on: nil, attempt)
        }
    }
    return attempt()
}

attempt(maximumRetryCount: 3) {
    flakeyTask(parameters: foo)
}.then {
    //…
}.catch { _ in
    // we attempted three times but still failed
}

Delegate变Promise

extension CLLocationManager {
    static func promise() -> Promise<CLLocation> {
        return PMKCLLocationManagerProxy().promise
    }
}

class PMKCLLocationManagerProxy: NSObject, CLLocationManagerDelegate {
    private let (promise, seal) = Promise<[CLLocation]>.pending()
    private var retainCycle: PMKCLLocationManagerProxy?
    private let manager = CLLocationManager()

    init() {
        super.init()
        retainCycle = self
        manager.delegate = self // does not retain hence the `retainCycle` property

        promise.ensure {
            // ensure we break the retain cycle
            self.retainCycle = nil
        }
    }

    @objc fileprivate func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        seal.fulfill(locations)
    }

    @objc func locationManager(_: CLLocationManager, didFailWithError error: Error) {
        seal.reject(error)
    }
}

// use:

CLLocationManager.promise().then { locations in
    //…
}.catch { error in
    //…
}

retainCycle是其中一个循环引用,目的是为了不让PMKCLLocationManagerProxy自身被释放,当Promise结束的时候,在ensure方法中执行self.retainCycle = nil把引用解除,来达到释放自身的目的,非常巧妙。

传递中间结果

有时候我们需要传递任务中的一些中间结果,比如下面的例子,done中无法使用username变量:

login().then { username in
    fetch(avatar: username)
}.done { image in
    //…
}

可以通过map巧妙的把结果变成元组形式返回:

login().then { username in
    fetch(avatar: username).map { ($0, username) }
}.then { image, username in
    //…
}

总结

尽管PromiseKit很多用法和原理都和Reactivecocoa相似,但它语法的简洁和直观是它最大的特点,光是这一点就足够吸引大家去喜欢它了~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • title: promise总结 总结在前 前言 下文类似 Promise#then、Promise#resolv...
    JyLie阅读 12,222评论 1 21
  • 00、前言Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区...
    夜幕小草阅读 2,128评论 0 12
  • Promiese 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,语法上说,Pr...
    雨飞飞雨阅读 3,348评论 0 19
  • 你不知道JS:异步 第三章:Promises 接上篇3-1 错误处理(Error Handling) 在异步编程中...
    purple_force阅读 1,384评论 0 2
  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,698评论 1 56