PromiseKit 工具的简单介绍及工程实践
这周来用用 PromiseKit 框架.
PromiseKit
是一个用于简化异步编程的工具, 它易学易用, 可以让代码更加简洁可读. 但体积较大(在 release 模式下编译的二进制包体积约 309 KB), 具体可参考 Google/Promises Benchmark.
简单使用
下面的内容都是抄自 PromiseKit Github 主页 :) .
then
方法, done
方法 以及 Promise
类型
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}
简单翻译过来就是: 先登录, 登录获取到响应数据后再获取用户头像, 获取用户头像图片后再图片设置到 imageView.
上述操作如果使用传统的 "完成块" 来写, 可能就会像下面这样了:
login { creds, error in
if let creds = creds {
fetch(avatar: creds.user) { image, error in
if let image = image {
self.imageView = image
}
}
}
}
果然没有对比就没有伤害...
其中的 then
块仅仅是"完成块"的一种组织方式, 而 done
和 then
类似, 只是它不能返回 promise. 可以将 done
看作一条 promise 链中的"成功"时执行末端.
下面来对比一下两个版本的 login
方法特征:
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
块中处理:
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
块中.
再来对比一下如果使用"完成块"来写这样上面相同功能的代码:
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
:
firstly {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
return login()
}.then {
fetch(avatar: $0.user)
}.done {
self.imageView = $0
}.ensure {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
//...
}
则无论链的执行成功或失败, ensure
块都会在那个点上执行.
对比一下"完成块"版本的实现代码:
// 在操作前设置
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
:
spinner(visible: true)
firstly {
foo()
}.done {
//…
}.catch {
//…
}.finally {
self.spinner(visible: false)
}
多操作并行执行并收集全部结果: when
函数
如果某个操作需要由多个异步前驱操作的结果作为参数, 且不使用任何工具的情况下, 其"完成块"版本可能会像下面这样丑陋且低效:
operation1 { result1 in
operation2 { result2 in
finish(result1, result2)
}
}
如果使用到了 GCD 的话, 则完成块版本可能会像下面那样:
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
函数, 情况就好很多了:
firstly {
when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
//...
}
when
函数接收一个或多个 promise, 等待它们的执行结果.
PromiseKit 对苹果 API 的扩展
PromiseKit 针对苹果 API 提供了许多扩展, 且这些扩展分布在不同的库中(和 RxSwift 与 RxCocoa 的关系类似). 所有扩展库目录详见这个链接.
UIKit
扩展在这里, Foundation
扩展在这里(包含 URLSession 的扩展), 此外针对一些热门库(貌似就只有 Alamofire ...) PromiseKit 也提供有一些扩展, 还有很多就不一一列举了..
写自己的 Promise
实际开发中还是有很多地方需要自己写 Promise 的, 下面就看看统一的套路.
比如有一个方法:
func fetch(completion: (String?, Error?) -> Void)
需要将它转换为 promise, 则:
func fetch() -> Promise<String> {
return Promise { fetch(completion: $0.resolve) }
}
上面代码去掉所有 "语法糖" 之后的版本如下, 方便理解:
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
:
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
类似, 只是语法上更简单了:
func fetch() -> Promise<String> {
return Guarantee { seal in
fetch { result in
seal(result)
}
}
}
"浓缩" 版本为:
func fetch() -> Promise<String> {
return Guarantee(resolver: fetch)
}
其他的一些"操作符"方法: map
, compactMap
等
-
then
方法的作用是在它的参数块中提供前一个 promise 的结果, 同时需要在参数块中返 回另外一个 promise. -
map
将前一个 promise 的结果进行处理, 并返回值或对象. -
compactMap
会在参数块中提供上一个 promise 的结果, 并且需要返回一个非 nil 的可选类型值, 如果返回nil
, 则会发生PMKError.compactMap
错误.
比如在网络请求的结果处理上, 经常会用到 compactMap
:
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
}
除了上面列举的, 还有很多"操作符" 方法, 详见文档.
两个群演: get
和 tap
这两个家伙很嬉皮, 但用处还挺大.
-
get
方法会在参数块中接收上一个 promise, 并返回该 promise. 这样可以在get
中写一些类似 SideEffect 的代码, 并可以通过它得知结果已收到. -
tap
方法用于 debugging, 只不过它的参数块中接收上一个 promise 的结果值, 返回上一个 promise, 这样就可以对上个 promise 的结果值进行检查了, 且不会对整个链造成任何影响.