PromiseKit 工具的简单介绍及工程实践

本文最后更新于 2021年4月4日 晚上

这周来用用 PromiseKit 框架.

PromiseKit 是一个用于简化异步编程的工具, 它易学易用, 可以让代码更加简洁可读. 但体积较大(在 release 模式下编译的二进制包体积约 309 KB), 具体可参考 Google/Promises Benchmark.

简单使用

下面的内容都是抄自 PromiseKit Github 主页 :) .

then 方法, done 方法 以及 Promise 类型

1
2
3
4
5
6
7
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}

简单翻译过来就是: 先登录, 登录获取到响应数据后再获取用户头像, 获取用户头像图片后再图片设置到 imageView.

上述操作如果使用传统的 “完成块” 来写, 可能就会像下面这样了:

1
2
3
4
5
6
7
8
9
login { creds, error in
if let creds = creds {
fetch(avatar: creds.user) { image, error in
if let image = image {
self.imageView = image
}
}
}
}

果然没有对比就没有伤害…

其中的 then 块仅仅是”完成块”的一种组织方式, 而 donethen 类似, 只是它不能返回 promise. 可以将 done 看作一条 promise 链中的”成功”时执行末端.

下面来对比一下两个版本的 login 方法特征:

1
2
3
4
5
func login() -> Promise<Creds>

// Compared with:

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

主要区别是: 使用 promise 的时候, 方法返回 promise 对象, 而非接收回调.

实际在 promise 链中的每个块都返回 promise 对象. Promise 类型的对象中有 then 方法, 这个方法的主要作用就是等待它所属的 promise 完成.

Promise 对象代表的是异步任务在”未来”的执行结果, 对象中包含了异步操作值的类型信息, 因为 Promise 是个泛型类型. 比如上面的 login 方法返回的就是 Promise<Creds>.

错误处理: catch 方法

使用 promise 时, error 沿 promise 链向下传递, 即整个链中产生的错误最后都汇集到 catch 块中处理:

1
2
3
4
5
6
7
8
9
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch {
// any errors in the whole chain land here
}

前面也说了, Promise 代表的是异步任务在未来的执行结果, 如果任务失败, 则它对应的 promise 就变为 rejected. 在一条链上如果产生了 rejected promise, 则后续的所有 then 都会被跳过, 转而进入后续的 catch 块去执行(实际上如果有多个 catch, 则都会被执行), 而相应的 error 也会被传递到 catch 块中.

再来对比一下如果使用”完成块”来写这样上面相同功能的代码:

1
2
3
4
5
6
7
8
9
10
11
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
}
}

哪个更可读一目了然.

SideEffect: ensure 方法

如果向 promise 链添加 ensure:

1
2
3
4
5
6
7
8
9
10
11
12
firstly {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
return login()
}.then {
fetch(avatar: $0.user)
}.done {
self.imageView = $0
}.ensure {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
//...
}

则无论链的执行成功或失败, ensure 块都会在那个点上执行.

对比一下”完成块”版本的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在操作前设置
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
}
}

如果使用完成块版本的话, 就不得不在出错和正常的情况下各设一次网络指示器的状态.

链真正的结束: finally 方法

finally 的作用是无论链的前方执行情况如何, 最终它都会被执行, 它就相当于处于链尾的 ensure:

1
2
3
4
5
6
7
8
9
10
11
spinner(visible: true)

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

多操作并行执行并收集全部结果: when 函数

如果某个操作需要由多个异步前驱操作的结果作为参数, 且不使用任何工具的情况下, 其”完成块”版本可能会像下面这样丑陋且低效:

1
2
3
4
5
operation1 { result1 in
operation2 { result2 in
finish(result1, result2)
}
}

如果使用到了 GCD 的话, 则完成块版本可能会像下面那样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}

那使用 when 函数, 情况就好很多了:

1
2
3
4
5
firstly {
when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
//...
}

when 函数接收一个或多个 promise, 等待它们的执行结果.

PromiseKit 对苹果 API 的扩展

PromiseKit 针对苹果 API 提供了许多扩展, 且这些扩展分布在不同的库中(和 RxSwift 与 RxCocoa 的关系类似). 所有扩展库目录详见这个链接.

UIKit 扩展在这里, Foundation 扩展在这里(包含 URLSession 的扩展), 此外针对一些热门库(貌似就只有 Alamofire …) PromiseKit 也提供有一些扩展, 还有很多就不一一列举了..

写自己的 Promise

实际开发中还是有很多地方需要自己写 Promise 的, 下面就看看统一的套路.

比如有一个方法:

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

需要将它转换为 promise, 则:

1
2
3
func fetch() -> Promise<String> {
return Promise { fetch(completion: $0.resolve) }
}

上面代码去掉所有 “语法糖” 之后的版本如下, 方便理解:

1
2
3
4
5
6
7
func fetch() -> Promise<String> {
return Promise { seal in
fetch { result, error in
seal.resolve(result, error)
}
}
}

其中 seal 对象由 Promise 类的构造函数提供, 通过 seal 对象提供的若干种方法就可以构造出想要的 Promise 对象了.

不会失败的 Promise: Guarantee<T>

Guarantee<T> 类型作为 Promise 的一种补充, 它的特性就是永不会失败, 一个例子就是使用 after 函数产生 Guarantee:

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

这个类型存在的意义就是协调 Swift 的错误处理系统. 因为使用 Promise 类的话, 在任何情况下均需要提供一个 catch 块, 否则 swift 就会警告说有未捕捉的错误.

创建 Guarantee<T>

和创建 Promise 类似, 只是语法上更简单了:

1
2
3
4
5
6
7
func fetch() -> Promise<String> {
return Guarantee { seal in
fetch { result in
seal(result)
}
}
}

“浓缩” 版本为:

1
2
3
func fetch() -> Promise<String> {
return Guarantee(resolver: fetch)
}

其他的一些”操作符”方法: map, compactMap

  • then 方法的作用是在它的参数块中提供前一个 promise 的结果, 同时需要在参数块中返回另外一个 promise.

  • map 将前一个 promise 的结果进行处理, 并返回值或对象.

  • compactMap 会在参数块中提供上一个 promise 的结果, 并且需要返回一个非 nil 的可选类型值, 如果返回 nil, 则会发生 PMKError.compactMap 错误.

比如在网络请求的结果处理上, 经常会用到 compactMap:

1
2
3
4
5
6
7
8
9
10
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
}

除了上面列举的, 还有很多”操作符” 方法, 详见文档.

两个群演: gettap

这两个家伙很嬉皮, 但用处还挺大.

  • get 方法会在参数块中接收上一个 promise, 并返回该 promise. 这样可以在 get 中写一些类似 SideEffect 的代码, 并可以通过它得知结果已收到.

  • tap 方法用于 debugging, 只不过它的参数块中接收上一个 promise 的结果值, 返回上一个 promise, 这样就可以对上个 promise 的结果值进行检查了, 且不会对整个链造成任何影响.

常用全局函数

一个良好的开端: firstly

firstly 函数的作用就类似”语法糖”, 仅仅是在参数块中提供一个 promise, firstly 函数会返回这个 promise, 从而作为整个 promise 链的开端, 便于代码的组织. 例如下面两段代码等价:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

// 1

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

// 2

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

when 函数群

when 函数有多种变体, 但类型只有下面三种:

  • when(fulfilled:)

    这个函数等待所有的 promise 完成, 如果遇到某个 promise 失败, 则它不再等待其它 promise, 而是直接失败, 从而跳到下一个 catch 执行.

    这里必须要注意的是, 虽然跳到了 catch, 但 when 没有终止其余 promise 的执行! 而只是简单地跳到 catch 执行而已!

    因为 promise 只是任务的封装, 而 promise 本身不具有对任务的任何控制能力, 故如果使用这种类型的 when, 则有必要在 catch 中手动取消其余 promise 中的任务操作.

  • when(resolved:)

    功能是等待所有 promise 完成, 不管有没有出现失败的 promise. 这类 when 函数的返回值是一个 Result<T> 类型的数组, 和 promise 的顺序一一对应.

    只不过… 它要求所有的 promise 的泛型类型一致…

  • race

    这类函数的作用是在多个 promise 间竞争, 最先到达的 promise 作为返回值. 比如有多个服务器提供相同数据的情况下, 使用这个函数就可以获取到最快服务器的数据了.

工程实践: 使用 PromiseKit + URLSession + 原生 JSON 编解码实现服务端数据获取及简单处理

这里抛开 iOS 端常用的两大网络通信框架 AlamofireAFNetworking, 直接使用 URLSession! 因为 URLSession 已经可以满足全部需求了!

工程代码详见”附”节.

  1. API 文档地址.

  2. 示例工程 Github.

  3. 代码量:

  1. 实践做法.

PromiseKit 工具的简单介绍及工程实践
https://blog.rayy.top/2018/11/03/2019-17-promise/
作者
貘鸣
发布于
2018年11月3日
许可协议