源码解读——PromiseKit
PromiseKit 是一款基于 Swift 的 Promise 异步编程框架,作者是大名鼎鼎的 Max Howell,Max Howell 同时也是 Homebrew 的作者,因在面试 Google 时写不出算法题反转二叉树而走红。
最近,我在研究 Promise 异步编程,一开始尝试从阅读 PromiseKit 源码上手,但是发现里面的一些设计理念难以理解。因此,转而去研究 Promise 核心原理,并产出了一篇文章——《Promise 核心实现原理》。最后,再回过头来阅读 PromiseKit 源码,很多当初不理解的设计立马豁然开朗了。这里,希望通过本文记录自己对于 PromiseKit 设计思想的一些理解。
注:本文分析的 PromiseKit 版本是 6.18.1
。
Thenable & CatchMixin
Thenable
是 PromiseKit
的核心协议之一,其声明了一个关键方法
pipe(to:)
,并实现了一系列链式操作符(方法),具体如下所示。
1 | public protocol Thenable: AnyObject { |
遵循 Thenable
协议的有两个泛型类型,分别是:
Promise<T>
:支持异步任务的成功和失败状态。Guarantee<T>
:仅支持异步任务的成功状态,不接受失败状态。
两者的主要区别在于 Promise
支持失败状态,而
Guarantee
不支持失败状态。因此,PromiseKit 定义了另一个协议
CatchMixin
,该协议声明并实现了错误处理相关的方法,如:
1 | public protocol CatchMixin: Thenable {} |
CatchMixin
协议继承自 Thenable
协议,从而限制 PromiseKit 只为遵循 Thenable
协议的类型支持
catch
和 recover
等能力。
Promise
从 《Promise 核心实现原理》 中,我们知道 Promise 内部主要包含几个部分:
- 执行状态:用于表示 Promise
的三种状态,
pending
、fulfilled
、rejected
。 - 执行结果:对于成功状态,为任务的返回值;对于失败状态,为任务的错误码。
- 回调任务列表:当 Promise 达到结束态时,将自动执行回调任务列表中的任务,并将当前执行结果作为参数传入回调任务。
- 执行器:用于更新执行状态和执行结果,并执行回调任务列表中的任务。
在 PromiseKit 的实现中,使用两个类型来表示 4 个部分:
Box
:表示一个容器,包含了 执行状态、执行结果、回调任务列表。Resolver
:执行器。
在 《Promise
核心实现原理》 中,我们设计实现的 Promise
强关联了回调任务列表和执行器,执行器同时反向强引用了 Promise。然而,在
PromiseKit 中,Promise
仅强关联了
Box
,而弱依赖了
Resolver
,Resolver
则强关联了
Box
。当然,两种设计在内存管理中的作用是一样的。如下所示为
Promise
与 Box
、Resolver
的引用关系。
下面,我们来分别介绍一下 Box
和 Resolver
的设计。
Box
PromiseKit 通过两种具体的 Box
子类来表示不同状态下的容器,分别是:
EmptyBox
:表示pending
状态下的容器。SealedBox
:表示resolved
状态下的容器,具体可以是fulfilled
或rejected
状态。
执行状态 & 执行结果 & 回调任务列表
Promise 通过 Sealant
枚举类型将三种执行状态(pending
、fulfilled
、rejected
)分成两种执行状态:
- 开始状态:
pending
状态。 - 结束状态:
resolved
状态,具体可以是fulfilled
或rejected
。
Sealant
的定义如下所示:
1 | enum Sealant<R> { |
其中,pending
状态的关联值存储了
回调任务列表
Handlers
;resolved
状态的关联值存储了两种细分的状态 Result
。
Result
枚举类型则用于进一步表示 fulfilled
和 rejected
状态,具体定义如下所示:
1 | public enum Result<T> { |
通过关联对象,分别存储两种 执行结果:返回值
T
和错误码 Error
。
EmptyBox & SealedBox
Box
抽象类定义了三个方法,分别是:
1 | class Box<T> { |
inspect()
方法用于检查内部状态,返回值为
Sealant
值。对于 SealedBox
,其返回始终为
resolved<T>
。
inspect(_ body: (Sealant<T>) -> Void)
方法将内部结果作为参数传递给闭包并执行。
seal(_: T)
方法非常关键,当 Box
为
pending
状态时,seal
方法可以将内部状态更新为
resolved
,同时执行 回调任务列表
中的所有任务。Resolver
就是通过 seal
方法来更新状态的。具体如下所示:
1 | class EmptyBox<T>: Box<T> { |
Resolver
Resolver
的核心作用是更新执行状态和执行结果,并执行回调任务列表中的任务。由于
Box
封装了执行状态、执行结果、回调任务列表,并且提供了更新状态的方法
seal
。因此,Resolver
只需提供针对不同状态的便利方法,内部调用 Box
的
seal
方法进行更新即可。具体如下所示。
1 | public final class Resolver<T> { |
任务串联
Promise 的核心能力之一是串联任务(异步或同步)。从
Thenable
的方法中,我们可以看到几乎所有的串联方法内部都是通过
pipe(to:)
方法实现任务串联的。下面,我们来看一下
Promise
中该方法的定义。
1 | public final class Promise<T>: Thenable, CatchMixin { |
从实现中可以看到,pipe(to:)
方法会先判断
Box
的状态,如果是 pending
状态,则将闭包加入回调任务列表;如果是 resolved
状态,则立即执行闭包。
内存管理
PromiseKit 所实现的 Promise
的内存管理是非常清晰。我们可以通过阅读各种链式操作符的内部实现来梳理其内存引用关系。如下所示,为链式操作符下产生的线性内存引用关系。
当对 promise 0
执行链式操作符时,链式操作符会创建一个
promise 1
,并在内部创建一个匿名闭包对用户闭包进行封装。同时,匿名闭包将引用
promise 1
,以处理 rejected
状态。
当 promise 0
为 fulfilled
时,执行匿名闭包,进而执行用户闭包,从而创建一个临时的
promise m
。此时,promise m
通过
pipe(to:)
方法将 promise 1
的状态更新方法(任务)box.seal
加入回调任务列表,最终形成上图所示的内存引用关系。
Guarantee
Guarantee
其实是裁剪版的
Promise
,不支持错误处理,即 保证有返回值
的含义。
Guarantee
遵循 Thenable
协议,并重新实现了一套链式操作符(方法),不同的是,返回值为
Guarantee
类型,从而避免交错使用 Promise
和
Guarantee
。
全局方法
为了便于使用,PromiseKit 还提供了几个常用的全局方法,包括:
after
:延时任务,Guarantee
类型。firstly
:语法糖,让代码更具可读性,立即执行闭包。race
:当有一个 Promise 为fulfilled
状态时,执行回调任务列表。when
:当所有 Promise 均为fulfilled
状态时,执行回调任务列表。
总结
整体而言,PromiseKit 以 Thenable
和
CatchMixin
协议为基础,实现了两种类型的 Promise,分别是
Promise
和 Guarantee
。
Promise
的工作流程主要涉及了 Box
和
Resolver
,两者有各自的职责。Box
包含了执行结果、执行状态、回调任务列表,并提供了状态更新方法
seal
。Resolver
作为执行器,提供给用户来更新状态。
Thenable
提供的 pipe(to:)
方法是串联任务的关键,几乎所有的链式操作符均使用了
pipe(to:)
方法进行任务串联。
从编码角度而言,PromiseKit 在职责单一、方法命名方面的设计与实践,还是非常值得我们来学习的。
后续,我们将继续阅读一些不错的开源源码,学习其中的设计思想。