PromiseKit 工具的简单介绍及工程实践
本文最后更新于 2021年4月4日 晚上
这周来用用 PromiseKit 框架.
PromiseKit
是一个用于简化异步编程的工具, 它易学易用, 可以让代码更加简洁可读. 但体积较大(在 release 模式下编译的二进制包体积约 309 KB), 具体可参考 Google/Promises Benchmark.
简单使用
下面的内容都是抄自 PromiseKit Github 主页 :) .
then
方法, done
方法 以及 Promise
类型
1 |
|
简单翻译过来就是: 先登录, 登录获取到响应数据后再获取用户头像, 获取用户头像图片后再图片设置到 imageView.
上述操作如果使用传统的 “完成块” 来写, 可能就会像下面这样了:
1 |
|
果然没有对比就没有伤害…
其中的 then
块仅仅是”完成块”的一种组织方式, 而 done
和 then
类似, 只是它不能返回 promise. 可以将 done
看作一条 promise 链中的”成功”时执行末端.
下面来对比一下两个版本的 login
方法特征:
1 |
|
主要区别是: 使用 promise 的时候, 方法返回 promise 对象, 而非接收回调.
实际在 promise 链中的每个块都返回 promise 对象. Promise
类型的对象中有 then
方法, 这个方法的主要作用就是等待它所属的 promise 完成.
Promise
对象代表的是异步任务在”未来”的执行结果, 对象中包含了异步操作值的类型信息, 因为 Promise
是个泛型类型. 比如上面的 login
方法返回的就是 Promise<Creds>
.
错误处理: catch
方法
使用 promise 时, error 沿 promise 链向下传递, 即整个链中产生的错误最后都汇集到 catch
块中处理:
1 |
|
前面也说了, Promise 代表的是异步任务在未来的执行结果, 如果任务失败, 则它对应的 promise 就变为 rejected. 在一条链上如果产生了 rejected promise, 则后续的所有 then
都会被跳过, 转而进入后续的 catch
块去执行(实际上如果有多个 catch, 则都会被执行), 而相应的 error 也会被传递到 catch
块中.
再来对比一下如果使用”完成块”来写这样上面相同功能的代码:
1 |
|
哪个更可读一目了然.
SideEffect: ensure
方法
如果向 promise 链添加 ensure
:
1 |
|
则无论链的执行成功或失败, ensure
块都会在那个点上执行.
对比一下”完成块”版本的实现代码:
1 |
|
如果使用完成块版本的话, 就不得不在出错和正常的情况下各设一次网络指示器的状态.
链真正的结束: finally
方法
finally
的作用是无论链的前方执行情况如何, 最终它都会被执行, 它就相当于处于链尾的 ensure
:
1 |
|
多操作并行执行并收集全部结果: when
函数
如果某个操作需要由多个异步前驱操作的结果作为参数, 且不使用任何工具的情况下, 其”完成块”版本可能会像下面这样丑陋且低效:
1 |
|
如果使用到了 GCD 的话, 则完成块版本可能会像下面那样:
1 |
|
那使用 when
函数, 情况就好很多了:
1 |
|
when
函数接收一个或多个 promise, 等待它们的执行结果.
PromiseKit 对苹果 API 的扩展
PromiseKit 针对苹果 API 提供了许多扩展, 且这些扩展分布在不同的库中(和 RxSwift 与 RxCocoa 的关系类似). 所有扩展库目录详见这个链接.
UIKit
扩展在这里, Foundation
扩展在这里(包含 URLSession 的扩展), 此外针对一些热门库(貌似就只有 Alamofire …) PromiseKit 也提供有一些扩展, 还有很多就不一一列举了..
写自己的 Promise
实际开发中还是有很多地方需要自己写 Promise 的, 下面就看看统一的套路.
比如有一个方法:
1 |
|
需要将它转换为 promise, 则:
1 |
|
上面代码去掉所有 “语法糖” 之后的版本如下, 方便理解:
1 |
|
其中 seal
对象由 Promise
类的构造函数提供, 通过 seal
对象提供的若干种方法就可以构造出想要的 Promise
对象了.
不会失败的 Promise
: Guarantee<T>
Guarantee<T>
类型作为 Promise
的一种补充, 它的特性就是永不会失败, 一个例子就是使用 after
函数产生 Guarantee
:
1 |
|
这个类型存在的意义就是协调 Swift 的错误处理系统. 因为使用 Promise
类的话, 在任何情况下均需要提供一个 catch 块, 否则 swift 就会警告说有未捕捉的错误.
创建 Guarantee<T>
和创建 Promise
类似, 只是语法上更简单了:
1 |
|
“浓缩” 版本为:
1 |
|
其他的一些”操作符”方法: map
, compactMap
等
then
方法的作用是在它的参数块中提供前一个 promise 的结果, 同时需要在参数块中返回另外一个 promise.map
将前一个 promise 的结果进行处理, 并返回值或对象.compactMap
会在参数块中提供上一个 promise 的结果, 并且需要返回一个非 nil 的可选类型值, 如果返回nil
, 则会发生PMKError.compactMap
错误.
比如在网络请求的结果处理上, 经常会用到 compactMap
:
1 |
|
除了上面列举的, 还有很多”操作符” 方法, 详见文档.
两个群演: get
和 tap
这两个家伙很嬉皮, 但用处还挺大.
get
方法会在参数块中接收上一个 promise, 并返回该 promise. 这样可以在get
中写一些类似 SideEffect 的代码, 并可以通过它得知结果已收到.tap
方法用于 debugging, 只不过它的参数块中接收上一个 promise 的结果值, 返回上一个 promise, 这样就可以对上个 promise 的结果值进行检查了, 且不会对整个链造成任何影响.
常用全局函数
一个良好的开端: firstly
firstly
函数的作用就类似”语法糖”, 仅仅是在参数块中提供一个 promise, firstly 函数会返回这个 promise, 从而作为整个 promise 链的开端, 便于代码的组织. 例如下面两段代码等价:
1 |
|
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 端常用的两大网络通信框架 Alamofire
和 AFNetworking
, 直接使用 URLSession
! 因为 URLSession
已经可以满足全部需求了!
工程代码详见”附”节.
附
代码量: