一些基本概念
在讲解PromiseKit 之前我们先来梳理一些基础概念:
同步和异步
这是一个老生常谈的问题,
同步操作意味着在操作完成之前,运行这个操作的线程都将被占用,直到函数最终被抛出或者返回。
尤其在 iOS 开发中,我们使用的UIKit并不是线程安全的:对用户输入的处理和 UI 的绘制,必须在与主线程绑定的 main runloop 中进行。假设我们希望用户界面以每秒 60 帧的速率运行,那么主线程中每两次绘制之间,所能允许的处理时间最多只有 16 毫秒 (1 / 60s)。当主线程中要同步处理的其他操作耗时很少时,这不会造成什么问题。但是,如果这个同步操作耗时过长的话,主线程将被阻塞。它不能接受用户输入,也无法向 GPU 提交请求去绘制新的 UI,这将导致用户界面掉帧甚至卡死。
而异步操作调用者不会立刻得到结果,两者之间主要的区别在于消息的通知机制 所以在开发过程中我们经常会遇到一些需要异步处理的相关业务,比如网络请求,文件io,还有一些耗时计算操作,通常在iOS中会这样处理异步逻辑,将耗时操作扔到子线程模块,在回调的时候提供运行在主线程的回调,以供 UI 操作之用
func myFunction(_ completion: @escaping (String?, Error?) -> Void) {
DispatchQueue.global().async {
do {
let d = try Data(contentsOf: URL(string: "")!) // 极其耗时
DispatchQueue.main.async {
completion(String(data: d, encoding: .utf8), nil)
}
} catch {
DispatchQueue.main.async {
completion(nil, error)
}
}
}
}
当然这种方式有着明显的弊端:
- 错误处理隐藏在回调函数的参数中,无法用 throw 的方式明确地告知并强制调用侧去进行错误处理。下游只能通过 if error 是否不为空
- 对回调函数的调用没有编译器保证,开发者可能会忘记调用 completion,或者多次调用 completion。
- 通过 DispatchQueue 进行线程调度很快会使代码复杂化。特别是如果线程调度的操作被隐藏在被调用的方法中的时候,不查看源码的话,在 (调用侧的) 回调函数中,几乎无法确定代码当前运行的线程状态。
- 对于正在执行的任务,没有很好的取消机制。
再举一个例子:这是一段常见的业务逻辑:
APIClient.fetchCurrentUser(success: { currentUser **in**
APIClient.fetchFollowers(user: currentUser, success: { followers **in**
// 现在你得到了一个 followers 数组
}, failure: { error **in**
// 错误处理
})
}, failure: { error **in**
// 错误处理
})
我们发现在经过第一层网络请求之后,我们又触发了二次的网络请求,且第二次依赖于第一次的返回结果,倘若我们再获取到第二次网络请求的结果还要进行第三次,我们要继续我们的嵌套结构,第四次第五次依次类推,很显然这种嵌套地狱是我们无法忍受的,而且代码可读性会变的异常糟糕,当然iOS 中提供了一些NSOPeration or GCD的解法,但是很显然逻辑不够紧凑,而且我的参数处理起来也是会有一些麻烦,所以PromiseKit 提供了很好的解决方案,像上面的例子在Primise中可以这样去实现
APIClient.fetchCurrentUser().then { currentUser in
return APIClient.fetchFollowers(user: currentUser)
}.done { followers in
// 你现在有了一个关注者数组
print("Followers: \(followers.map { $0.name })")
}.catch { error in
// 出现错误时执行
print("Error: \(error)")
}
我们来实现一下 fetchCurrentUser和 fetchFollowers ,实现就变成了这样:
static func fetchCurrentUser() -> Promise<User> {
return Promise { seal in
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
// 模拟成功获取当前用户
let currentUser = User(id: 1, name: "John Doe")
seal.fulfill(currentUser)
// 或者模拟一个错误
// seal.reject(Error(domain: "com.example.error", code: 404, userInfo: nil))
}
}
}
static func fetchFollowers(user: User) -> Promise<[Follower]> {
return Promise { seal in
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
// 模拟成功获取关注者列表
let followers = [
Follower(id: 2, name: "Jane Doe"),
Follower(id: 3, name: "Jim Beam"),
Follower(id: 4, name: "Jack Daniels")
]
seal.fulfill(followers)
// 或者模拟一个错误
// seal.reject(Error(domain: "com.example.error", code: 404, userInfo: nil))
}
}
}
我们要实现Promise的链式调用,原函数自然应该返回一个promise,这有点像flutter中的future 很显然这样的写法没有了前面的嵌套地狱,Promise 是将嵌套/缩进样式的代码变成一个层级的代码,于此同时我们注意到多了很多的陌生的名词和语法糖:Promise Then catch seal fulfill reject,下面我们来展开分析一下
在Promise的核心实现中,主要有两大核心部分,分别是个Box 和 Resolver
-
Box:表示一个容器,包含了 执行状态、执行结果、回调任务列表。 -
Resolver:执行器。 Promise 通过Sealant枚举类型将三种执行状态(pending、fulfilled、rejected)分成两种执行状态: -
开始状态:
pending状态。 -
结束状态:
resolved状态,具体可以是fulfilled或rejected。
Sealant 的定义如下所示:
// 枚举 Sealant,用于表示 Promise 的状态
enum Sealant<R> {
case pending(Handlers<R>)
case resolved(R)
}
final class Handlers<R> {
var bodies: [(R) -> Void] = []
func append(_ item: @escaping(R) -> Void) { bodies.append(item) }
}
其中,pending 状态的关联值存储了 回调任务列表 Handlers;resolved 状态的关联值存储了两种细分的状态 Result。
Result 枚举类型则用于进一步表示 fulfilled 和 rejected 状态,具体定义如下所示:
public enum Result<T> {
case fulfilled(T)
case rejected(Error)
}
fulfill 、reject 、peding
fulfill 、reject 、peding分别用于表示 Promise 的三种状态,
Promise 内部的状态由 执行器(executor) 或 解析器(resolver) 来进行更新。Promise 创建时的状态默认为 pending,用户为 Promise 提供状态转移逻辑,比如:网络请求成功时将状态设置为 fulfilled,网络请求失败时将状态设置为 rejected。通常,执行器会提供两个方法 resolve 和 reject 分别用于设置 fulfilled 和 rejected 状态。
Box 抽象类定义了三个方法,分别是:
class Box<T> {
func inspect() -> Sealant<T> { fatalError() }
func inspect(_: (Sealant<T>) -> Void) { fatalError() }
func seal(_: T) {}
}
inspect() 方法用于检查内部状态,返回值为 Sealant 值。对于 SealedBox,其返回始终为 resolved<T>。
inspect(_ body: (Sealant<T>) -> Void) 方法将内部结果作为参数传递给闭包并执行。
seal(_: T) 方法非常关键,当 Box 为 pending 状态时,seal 方法可以将内部状态更新为 resolved,同时执行 回调任务列表** 中的所有任务。Resolver 就是通过 seal 方法来更新状态的。具体如下所示:
class EmptyBox<T>: Box<T> {
...
override func seal(_ value: T) {
var handlers: Handlers<T>!
barrier.sync(flags: .barrier) {
guard case .pending(let _handlers) = self.sealant else {
return // already fulfilled!
}
handlers = _handlers
self.sealant = .resolved(value)
}
if let handlers = handlers {
handlers.bodies.forEach{ $0(value) }
}
}
}
Resolver
Resolver 的核心作用是更新执行状态和执行结果,并执行回调任务列表中的任务。由于 Box 封装了执行状态、执行结果、回调任务列表,并且提供了更新状态的方法 seal。因此,Resolver 只需提供针对不同状态的便利方法,内部调用 Box 的 seal 方法进行更新即可。具体如下所示。
public final class Resolver<T> {
let box: Box<Result<T>>
init(_ box: Box<Result<T>>) {
self.box = box
}
deinit {
if case .pending = box.inspect() {
conf.logHandler(.pendingPromiseDeallocated)
}
}
}
public extension Resolver {
/// Fulfills the promise with the provided value
func fulfill(_ value: T) {
box.seal(.fulfilled(value))
}
/// Rejects the promise with the provided error
func reject(_ error: Error) {
box.seal(.rejected(error))
}
/// Resolves the promise with the provided result
func resolve(_ result: Result<T>) {
box.seal(result)
}
...
}
此外,Promise 还支持通过链式操作符实现回调任务的链式执行,其原理是在内部维护一个回调任务列表,当 Promise 到达结束状态时,自动执行内部的回调任务,从而整体实现异步任务的链式执行。
Then
- 简单来讲,then 方法就是把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
- 而 Promise 的优势就在于这个链式调用。我们可以在 then 方法中继续写 Promise 对象并返回,然后继续调用 then 来进行回调操作。 我们来看一下这个Then的内部实现:
// 定义 Result 枚举,用于表示 Promise 的结果
public enum Result<T> {
case fulfilled(T)
case rejected(Error)
}
/// Thenable represents an asynchronous operation that can be chained.
public protocol Thenable: AnyObject {
associatedtype T
/// `pipe` is immediately executed when this `Thenable` is resolved
func pipe(to: escaping (Result<T>) -> Void)
/// The resolved result or nil if pending.
var result: Result<T>? { get }
}
/// - See: `Thenable.pipe`
public func pipe(to: escaping(Result<T>) -> Void) {
switch box.inspect() {
case .pending:
box.inspect {
switch $0 {
case .pending(let handlers):
handlers.append(to)
case .resolved(let value):
to(value)
}
}
case .resolved(let value):
to(value)
}
}
从实现中可以看到,`pipe(to:)` 方法会先判断 `Box` 的状态,如果是 `pending` 状态,则将闭包加入回调任务列表;如果是 `resolved` 状态,则立即执行闭包。
func then<U: Thenable>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil _ body: escaping (T) throws -> U) -> Promise<U.T> {
let rp = Promise<U.T>(.pending)
pipe {
switch $0 {
case .fulfilled(let value):
on.async(flags: flags) {
do {
let rv = try body(value)
guard rv !== rp else { throw PMKError.returnedSelf }
rv.pipe(to: rp.box.seal)
} catch {
rp.box.seal(.rejected(error))
}
}
case .rejected(let error):
rp.box.seal(.rejected(error))
}
}
return rp
}
PromiseKit 相关使用
when() 方法
- when 方法提供了并行执行异步操作的能力,并且只有在所有异步操作执行完后才执行回调。
- 和其他的 promise 链一样,when 方法中任一异步操作发生错误,都会进入到下一个 catch 方法中。
when(resolved: promise1, promise2, promise3).then { results in
for result in results where case .fulfilled(let value) {
//…
}
}.catch { error in
// invalid! Never rejects
}
当我们需要批量处理一些异步逻辑,这是一个非常常见的场景,当然我们可以通过NSOperation 去实现。虽然 NSOperation 拥有一个完成回调以及操作的状态,但它不能保存得到的值,你需要自己去管理。而且
NSOperation 还持有线程模型以及优先级顺序相关的数据,而 Promise 对代码如何完成不做任何保证,
race() 方法
- race 按字面解释,就是赛跑的意思。race 的用法与 when 一样,只不过 when 是等所有异步操作都执行完毕后才执行 then 回调。而 race 的话只要有一个异步操作执行完毕,就立刻执行 then 回调。 举个常见的例子,我们经常会判断一些业务引擎是否启动超时,亦或者我们要拿到率先完成的异步操作,并获取到结果 我们就可以用到race:
race(promise1(), promise2()).done{ data in
print("结果:(data)")
}
recover() 方法
recover 是另一个有用的函数。它可以捕获一个错误,然后轻松地恢复状态,同时不会弄乱其余的 Promise 链。
我们很清楚这个函数的形式:它应该接受一个函数,该函数中接受错误并返回新的 Promise。recover 方法也应该返回一个 Promise 以便继续链接 Promise 链。
有了这个新的函数就可以从错误中恢复。例如,如果网络没有加载我们期望的数据,可以从缓存中加载数据:
APIClient.getUsers()
.recover({ error in
return cache.getUsers()
}).then({ user in
//更新 UI
}).catch({ error in
//错误处理
})
当然我们也可以自行扩展retry逻辑: 重试是我们可以添加的另一个功能。若要重试,需要指定重试的次数以及一个能够创建 Promise 的函数,该 Promise 包含了重试要执行的操作(所以这个 Promise 会被重复创建很多次)。
public static func retry<T>(count: Int, delay: TimeInterval, generate: @escaping () -> Promise<T>) -> Promise<T> {
if count <= 0 {
return generate()
}
return Promise<T>(work: { fulfill, reject in
generate().recover({ error in
return self.delay(delay).then({
return retry(count: count-1, delay: delay, generate: generate)
})
}).then(fulfill).catch(reject)
})
}
after() 方法
我们经常会对代码逻辑进行一些延迟执行 通过gcd的相关api当然可以做到,当然PromiseKit提供了更简洁的方式来处理:
after(seconds: 5).done {
print``("欢迎访问hangge.com")
}
总结:
Promise 是异步任务的执行过程,表示一个值的生产过程,Promise中还有很多操作值得去发现和扩展,可以去 github.com/mxcl/Promis… 仔细研究