这周来用用 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 的结果值进行检查了, 且不会对整个链造成任何影响.
常用全局函数
一个良好的开端: firstly
firstly 函数的作用就类似“语法糖”, 仅仅是在参数块中提供一个 promise, firstly 函数会返回这个 promise, 从而作为整个 promise 链的开端, 便于代码的组织. 例如下面两段代码等价:
// 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 端常用的两大网络通信框架 Alamofire 和 AFNetworking, 直接使用 URLSession! 因为 URLSession 已经可以满足全部需求了!
工程代码详见“附”节.
附
-
代码量:
