PromiseKit 基础部分

全部文章

  • 简介
  • 基础部分
    • 快速上手
    • Promise 的常见模式
    • 常见问题
  • 进阶部分
    • 故障排除
    • 附录
  • API 说明

以下是对 PromiseKitREADME.md 的翻译。

then and done

下面是一个典型的 promise 调用链:

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

如果改为使用完成处理程序来实现,则会像下面一样:

login { creds, error in
    if let creds = creds {
        fetch(avatar: creds.user) { image, error in
            if let image = image {
                self.imageView = image
            }
        }
    }
}

then 看起来只是实现完成处理程序的另一种实现,但是它可以提供的更多。当我们开始去理解代码的时候,它的可读性更高。上面的 promise 链非常容易扫视并且理解:一个异步操作接着另一个,一行接着一行。对于现阶段的 swift,它非常接近于过程代码。

donethen 一样,但是不能再返回一个 promise 了。它经常用在调用链的末尾,用于处理成功的情况。上面代码中,你可以发现在 done 中,我们拿到了最终的 image,并用它来设置 UI。

我们来对比一下两种实现 login 的方法:

func login() -> Promise
    
// Compared with:

func login(completion: (Creds?, Error?) -> Void)
                        // ^^ ugh. Optionals. Double optionals.

使用 promise 的差别在于,你的方法需要返回一个 promise,而不是接收并执行一个回调。在链上的每个处理程序都要返回一个 promise。Promise 类定义了 then 方法,他会在继续执行链之前等待给定的 promise 执行完成。在程序上,调用链一次只能执行一个 promise。

一个 Promise 代表了一个异步执行的结果。他有一个类型来表示它包含的类的类型。例如,在上面的例子中,login 是一个返回代表 Creds 实例的 Promise 的方法。

注意:done 是 PromiseKit 5 中的新功能。我们之前定义了一个 then 的变体,它允许你不返回一个 Promise。不幸的是,这个约定经常使得 Swift 混淆,并且会导致一些奇怪的、难以排查的错误信息。它也使得 PromiseKit 难以使用。done 的引入,使我们在写 promise 链时,不需要再添加额外的类型信息,来用于帮助编译器推断类型信息了。

你可能注意到了,这和使用 completion 回调的方式有所不同,它忽略了 error。事实上并非如此,相反,在错误处理方法,Promise 更加的方便,而且很难忽略。

catch

在 Promise 中,异常会沿着 Promise 链传递,这可以确保你的 app 的健壮性,而且代码清晰。

firstly {
    login()
}.then { creds in
    fetch(avatar: creds.user)
}.done { image in
    self.imageView = image
}.catch {
    // any errors in the whole chain land here
}

如果你忘记了在 chain 中使用 catch,swift 将会发出警告。我们将会在稍后介绍更多细节。

每个 promsie 类代表着单个异步任务。如果一个任务失败,则对应的 Promise 将会变成 rejected 状态。在链中,一旦一个 promise 变成 rejected 状态,它将忽略之后的 then,直接去执行下一个 catch。(严格来说,他会执行所所有继的 catch 处理程序。)

我们来对比一下这种情况和它对应的使用 completion 回调的处理程序:

func handle(error: Error) {
    //…
}

login { creds, error in
    guard let creds = creds else { return handle(error: error!) }
    fetch(avatar: creds.user) { image, error in
        guard let image = image else { return handle(error: error!) }
        self.imageView.image = image
    }
}

在可读性方面,上面的代码虽然使用了 guard 和一个固定的错误处理程序,和 Promise 链比起来,不值一提。

ensure

我们已将学会了写一个异步任务,接下来,我们来扩展语法:

firstly {
    UIApplication.shared.isNetworkActivityIndicatorVisible = true
    return login()
}.then {
    fetch(avatar: $0.user)
}.done {
    self.imageView = $0
}.ensure {
    UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
    //…
}

不管你的调用链的结果怎样——成功还是失败—— ensure 处理程序将始终会被调用。

我们来对比一下这种模式和它对应的 completion 回调的方式:

UIApplication.shared.isNetworkActivityIndicatorVisible = true

func handle(error: Error) {
    UIApplication.shared.isNetworkActivityIndicatorVisible = false
    //…
}

login { creds, error in
    guard let creds = creds else { return handle(error: error!) }
    fetch(avatar: creds.user) { image, error in
        guard let image = image else { return handle(error: error!) }
        self.imageView.image = image
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
    }
}

在修改这段代码时,很容易就会忘记重置指示器,也就会导致 bug。在使用 Promise 时,这种错误基本上是不可能发生的。swift 编译器将会强制你使用 Promise 来补充调用链。你基本不需要 review 提交的代码。

注意:PromiseKit 曾经几次将这个功能反复的命名为 alwaysensure。对此感到抱歉。

当 ensure 用于终止 promise 链并且没有返回值时,你也可以使用 finally

spinner(visible: true)

firstly {
    foo()
}.done {
    //…
}.catch {
    //…
}.finally {
    self.spinner(visible: false)
}

when

在使用 completion 回调时,对多个异步操作的处理即慢又麻烦。慢是指它是依次处理的:

operation1 { result1 in
    operation2 { result2 in
        finish(result1, result2)
    }
}

快速(并行)的代码将会降低代码的可读性:

var result1: …!
var result2: …!
let group = DispatchGroup()
group.enter()
group.enter()
operation1 {
    result1 = $0
    group.leave()
}
operation2 {
    result2 = $0
    group.leave()
}
group.notify(queue: .main) {
    finish(result1, result2)
}

使用 Promise 就非常简单:

firstly {
    when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
    //…
}

when 接收多个 Promise,它将会等待传入的 promise 执行结束,并且返回一个包含这里 promsie 结果的 promise。

与其他的 Promise 链一样,传入的 promise 中任何一个失败,调用链都会调用下一个 catch。

PromiseKit Extensions

当制作 PromiseKit 框架时,我们发现只有在实现异步操作的时候,我们才会使用 Promise。因此,只要有可能,我们都对苹果的 API 提供了符合 promise 的扩展。例如:

firstly {
    CLLocationManager.promise()
}.then { location in
    CLGeocoder.reverseGeocode(location)
}.done { placemarks in
    self.placemark.text = "\(placemarks.first)"
}

使用这些扩展时,需要添加下面的 subspecs:

pod "PromiseKit"
pod "PromiseKit/CoreLocation"
pod "PromiseKit/MapKit"

在 PromiseKit organization 中可以看到所有可用的扩展。你可以点击链接查看所有可用的内容,也可以阅读源码和文档。所有的文件和方法都有丰富的说明文档。

我们也对通用的库进行了扩展,例如:Alamofire。

创建 Promises

标准的扩展可以应对绝大部分的场景,但有时你还是得写一些特殊的调用链。例如:你使用的第三方库没有提供 Poromise 的 API,或者你自己写的一些异步功能。无论哪种情况,添加 promise 扩展都非常的简单。当你查看标准扩展的源码时,你会发现它都是按照如下的方法来实现的。

假设我们有如下所示的方法:

func fetch(completion: (String?, Error?) -> Void)

我们应该如何改造成 promise 类型的方法呢?非常简单:

func fetch() -> Promise {
    return Promise { fetch(completion: $0.resolve) }
}

下面的扩展版本可读性更高一些:

func fetch() -> Promise {
    return Promise { seal in
        fetch { result, error in
            seal.resolve(result, error)
        }
    }
}

Promise 初始化了一个 seal 类给我我们,seal 不仅提供了一些处理常见结果的方法,甚至涵盖了各种罕见的情况,因为我们可以轻松的向现有代码库添加 promise 扩展。

注意:我们尝试着只用 Promise(fetch) 来让方法更简单,但是没能做到,因为时常需要提供额外的信息,swift 编译器才能顺利的编译。很抱歉,我们没能做到这一点。

注意:在 PMK 4 中,初始化程序提供了两个参数来处理不同的结果: fulfill 和 reject。在 PMK 5 和 6 中,只提供了包含 fulfill 和 reject 方法的一个类,但是它可以处理更多的情况。通常,你只需要将处理程序传递给 resolve 方法,然后让 swift 根据具体情况来适配 resolve 的参数(如上所示)。

注意:Guarantees(下面会介绍)的初始化程序稍有不同,因为 Guarantees 只能处理成功的情况,所以初始化程序的闭包参数只是一个闭包,而不是 Resolver 对象。所以,需要执行 seal(value) 而不是 seal.fulfill(value)。这是因为 Guarantees 只处理 fulfill,而不处理其他情况,

Guarantee

从 PromiseKit 5 开始,我们提供了 Guarantee 类来作为 Promise 的补充。这样做为补充 swift 强大的错误处理系统。

Guarantees 表示没有失败的情况,所以他们没有 rejected 状态。一个很好的例子就是 after :

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

如果你不终止常规的 Promise 调用链,swift 将会发出警告。你应该通过 catch 或者 retrun 来消除警告。(在下面的例子中,你只能在 promise 的末尾使用 catch。)

让你的程序中,尽可能的使用 Guarantees,在需要处理错误情况时,写错误处理程序,在不需要时,不写错误处理程序。

通常情况下,GuaranteePromise 可以互换使用。为了确保这一点,我们已经做了足够多的测试,如果你发现还有问题,请和我们联系。

创建 guarantees 的语法和 promises 相比,更加的简单。

func fetch() -> Promise {
    return Guarantee { seal in
        fetch { result in
            seal(result)
        }
    }
}

这还可以简化为:

func fetch() -> Promise {
    return Guarantee(resolver: fetch)
}

map, compactMap, etc.

then 提供了上一个 promise 的结果,需要返回另一个 promise。

map 提供了上一个 promise 的结果,需要返回一个类或者结果类型。

compactMap 提供了上一个 promise 的结果,需要返回一个可选类型。如果返回 nil,调用链就会抛出 PMKError.compactMap 的错误而失败。

说明:在 PromiseKit 4 之前,then 处理了所有的情况,这很糟糕。我们希望在新的 Swift 版本中能有所好转。但是,很明显,糟糕情况依然存在。事实上,我们作为框架的作者,非常希望能在命名上消除 API 的歧义。所以我们将 then 的三种含义拆分为 thenmapdone。在实践中,我们发现这些新功能更加的好用,所以我们有添加了 compactMap(参考了 Optional.compactMap 方法)。

我们可以使用 compactMap 更加方便的创建 promise 链。例如:

firstly {
    URLSession.shared.dataTask(.promise, with: rq)
}.compactMap {
    try JSONSerialization.jsonObject($0.data) as? [String]
}.done { arrayOfStrings in
    //…
}.catch { error in
    // Foundation.JSONError if JSON was badly formed
    // PMKError.compactMap if JSON was of different type
}

提醒:我们还提供了很多你可能会在数组中用到的方法,例如:mapthenMapcompatMapValuesfirstValue,等等。

get

get 就行 done 一样,不过他可以返回结果流。

firstly {
    foo()
}.get { foo in
    //…
}.done { foo in
    // same foo!
}

tap

tap 用于测试。看起来和 get 一样,但是它提供了 promise 的 Result,所以你可以在某处查看调用链的值,而不产生任何副作用:

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

补充

firstly

我们已经使用了 firstly 几次,但是它究竟是什么?事实上,它就是一个语法糖。你不一定需要它,但是它可以让你的调用链的可读性更高。可以将下面的代码进行替换:

firstly {
    login()
}.then { creds in
    //…
}

替换为:

login().then { creds in
    //…
}

理解这个很关键:login() 返回一个 Promise,并且所有的 Promise 有一个 then 方法。firstly 返回一个 Promise,并且 then 也返回一个 Promise。不用太关心其中的细节。先了解这种模式,等你想进一步了解的时候,再去学习底层的架构。

when 的变体

在 PromiseKit 当中,when 是一个非常有用的方法,所以,我们提供了几种变体。

  • 默认的 when,也就是你通常用的是 when(fulfilled:) 。这种情况是,组成 when 的所有 promise 中,只要有一个失败,则 when 就会失败, 接着调用链就会变成 rejects 状态。有一点非常重要:构成 when 的所有 Promise 都会继续执行。Promise 没有可以控制他们包含的任务。Promise 只是将任务进行了一层包装。
  • when(resolved:) 这个变体是:组成 when 的所有 Promise 中即使有一个或多个 Promise 失败了,它依然提供了一个 Result 的结果。因为,这个变体要求所有构成 when 的 Promise 有相同的泛型。这个限制如何解除,可以查看指南的进阶部分。
  • race 变体可以让几个 Promise 进行竞争。哪个先结束,结果就是哪个。更详细的用法可以查看指南的进阶部分。

Swift 闭包推断

对于一行的闭包,swift 会自行推断返回值和返回值的类型。下面的两种形式效果一样:

foo.then {
    bar($0)
}

// is the same as:

foo.then { baz -> Promise in
    return bar(baz)
}

在文档中,为了更加的简洁,我们经常省略掉 return

但是,这种简写既是福,又是祸。你会发现 Swift 编译器经常不能准确的推断出返回值类型。你可以查看 Troubleshooting Guide 来获取更多的帮助。

在 PromiseKit 5 中,可以通过使用 done 来避免这个痛点。

进一步学习

在使用的过程中,上面的内容涵盖了 90% 的情况。强烈的建议你再阅读一下 API Reference。这里有很多对你来说非常有用的小功能,其中的文档也将会对上述内容从源码层面进行更详细的说明。

你可能感兴趣的:(PromiseKit 基础部分)