Promise 核心实现原理
在传统的基于 闭包 的异步编程中,经常会出现 地狱嵌套 的问题,这使得高度异步的代码几乎无法阅读。Promise 则是解决这个问题的众多方案之一。
Promise 的核心思想是:实现一个容器,对内管理异步任务的执行状态,对外提供同步编程的代码结构,从而具备更好的可读性。本文,我们将通过分析 Promise 的设计思想,并实现 Promise 核心逻辑,从而深入理解 Promise 实现原理。
本文所实现的 Promise 代码已在 Github 开源——传送门。
Future vs. Promise
Future 和 Promise 是异步编程中经常提到的两个概念,两者的关系经常用一句话来概括——A Promise to Future。
我们可以认为 Future 和 Promise 是一种异步编程技术的两个部分:
- Future 是异步任务的返回值,表示一个未来值的占位符,是值的消费者。
- Promise 是异步任务的执行过程,表示一个值的生产过程,是值的生产者。
以如下一段 Dart 代码为例,getUserInfo
方法体是一个
Promise,其定义了值的生产过程,getUserInfo
方法返回值是一个
Future,其定义了一个未来值。
1 | Future<UserInfo> getUserInfo(BuildContext context) async { |
Future 和 Promise 来源于函数式编程语言,其目的是分离一个值和生产值的方法,从而简化异步编程。本质上,两者是一一对应的。
很多语言都有 Future 和 Promise 的实现,比如:Swift Task、C# Task、C++ std::future、Scala Future 对应的是 Future 的实现;C++ std::promise、JavaScript Promise、Scala Promise 对应的是 Promise 的实现。
基本用法
Promise
支持以同步代码结构编写异步代码逻辑,其提供一系列便利方法以支持链式调用,如:then
、done
、catch
、finally
等。注意,不同的编程语言或库实现中,方法命名有所不同。
如下所示,是一个以 JavaScript 编写的 Promise 的基本用法。
1 | getJSON("/post/1.json") |
基本原理
本质上,Promise 是一个对象,其包含三种状态,分别是:
pending
:表示进行中状态。fulfilled
:表示已成功状态状态。此时,Promise 得到一个结果值value
。rejected
:表示已失败状态。此时,Promise 得到一个错误值error
,用于表示错误原因。
pending
是起始状态,fulfilled
和
rejected
是结束状态。一旦 Promise
的状态发生了变化,它将不会再改变。因此,Promise 是一种
单赋值 的结构。
Promise 内部的状态由 执行器(executor) 或
解析器(resolver) 来进行更新。Promise
创建时的状态默认为 pending
,用户为 Promise
提供状态转移逻辑,比如:网络请求成功时将状态设置为
fulfilled
,网络请求失败时将状态设置为
rejected
。通常,执行器会提供两个方法 resolve
和 reject
分别用于设置 fulfilled
和
rejected
状态。
此外,Promise 还支持通过链式操作符实现回调任务的链式执行,其原理是在内部维护一个回调任务列表,当 Promise 到达结束状态时,自动执行内部的回调任务,从而整体实现异步任务的链式执行。
核心实现
下面,我们来手动实现 Promise 的核心逻辑,编程语言为 Swift。
状态
首先,定义 Promise 的三个状态,如下所示。 1
2
3
4
5enum State {
case pending
case fulfilled
case rejected
}
执行器
Promise 的核心目标是为了解决异步(或同步)任务的相关问题。首先,要解决两个问题:
- 如何表示异步任务?
- 如何更新任务状态?
对于第一个问题,很简单,我们可以提供一个闭包,让用户在闭包中自定义任务即可。
对于第二个问题,同样,我们可以提供两个状态更新方法,让用户在任务的特定阶段调用即可。
这里,我们定义的执行器如下所示。 1
2
3
4
5class Promise<T> {
typealias Resolve<T> = (T) -> Void
typealias Reject = (Error) -> Void
typealias Executor = (_ resolve: @escaping Resolve<T>, _ reject: @escaping Reject) -> Void
}
可以看到,上述定义的执行器是一个闭包,闭包的参数是两个状态更新方法,分别是
resolve
和
reject
,可供用户在任务的特定阶段调用,以更新任务的状态。
由于 resolve
和 reject
方法分别用于设置
fulfilled
和 rejected
状态,两个状态分别对应两个值:value
和
error
,从方法的入参可以看出两者的区别。因此,除了状态之外,还需定义两个字段,分别用于保存
value
和 error
,具体定义如下所示。
1 | class Promise<T> { |
链式执行
Promise 的核心功能之一是 链式执行
异步任务。那么,如何实现链式执行异步任务呢?很简单,我们将后一个 Promise
的异步任务存储在前一个 Promise 的回调任务列表中,当前一个 Promise
达到结束状态(fulfilled
或
rejected
)时,执行其内部保存的下一个(组)回调任务即可。
对此,我们可以在 Promise 内部保存两个数组,分别用户存储
fulfilled
状态和 rejected
状态时要执行的回调任务。除此之外,我们还需要对 resolve
和
reject
方法进行进一步加工,方法调用时,分别设置当前异步任务的返回值、状态,并执行回调任务。具体定义如下所示。
1 | class Promise<T> { |
可以看到,我们分别使用 onFulfilledCallbacks
和
onRejectedCallbacks
保存回调任务。同时定义了
resolve
和 reject
两个方法,内部分别设置异步任务的返回值、状态,并执行回调任务。
Promise 初始化时,执行器会立即执行,从而触发异步任务的执行,同时将两个状态更新方法作为参数传入闭包,以供用户在任务的特定阶段调用。
任务串联
Promise 通过 then
方法来串联任务,即让前一个 Promise
保存下一个 Promise 的任务。then
方法包含两个闭包
onFulfilled
和
onRejected
,分别表示不同状态的回调任务,其在前一个 Promise
的状态为 fulfilled
和 rejected
时分别执行。
当 then
串联任务时,我们需要考虑前一个 Promise
的状态。这里,我们分三种情况进行考虑:
- 当前一个 Promise 的状态为
pending
时,我们创建一个 Promise,其任务的核心是将onFulfilled
和onRejected
分别加入前一个 Promise 的回调任务队列中。 - 当前一个 Promise 的状态为
fulfilled
时,我们创建一个 Promise,其任务的核心是立即执行onFulfilled
任务。 - 当前一个 Promise 的状态未
rejected
时,我们创建一个 Promise,其任务的核心是立即执行onRejected
任务。
then
方法的具体实现如下所示。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37extension Promise {
// Functor
@discardableResult
func then<R>(onFulfilled: @escaping (T) -> R, onRejected: @escaping (Error) -> Void) -> Promise<R> {
switch state {
case .pending:
// 将普通函数应用到包装类型,并返回包装类型
return Promise<R> { [weak self] resolve, reject in
// 初始化时即执行
// 在 curr promise 加入 onFulfilled/onRejected 任务,任务可修改 curr promise 的状态
self?.onFulfilledCallbacks.append { value in
let r = onFulfilled(value)
resolve(r)
}
self?.onRejectedCallbacks.append { error in
onRejected(error)
reject(error)
}
}
case .fulfilled:
let value = value!
// 将普通函数应用到包装类型,并返回包装类型
return Promise<R> { resolve, _ in
let r = onFulfilled(value)
resolve(r)
}
case .rejected:
let error = error!
// 将普通函数应用到包装类型,并返回包装类型
return Promise<R> { _, reject in
onRejected(error)
reject(error)
}
}
}
}
注意,onFulfilled
和 onRejected
闭包的入参和返回值,这是 then
能够实现异步任务的值传递的关键。
Monad
上一节的 then
方法主要是 Functor 实现,为了进一步扩展
then
方法的,我们来实现 Monad then
方法,具体实现如下所示。
关于 Functor 和 Monad 的概念,可以阅读 《函数式编程——Functor、Applicative、Monad》。
1 | extension Promise { |
便利方法
通常 Promise
还具有一系列遍历方法,如:fistly
、catch
、done
、finally
等。下面,我们依次实现。
firstly
方法本质上是语法糖,表示异步任务组的第一步。我们实现一个全局方法,通过闭包实现任务的具体逻辑,如下所示。
1
2
3func firstly<T>(closure: @escaping () -> Promise<T>) -> Promise<T> {
return closure()
}
catch
方法仅用于处理错误,其可通过 then
方法实现,关键是实现 onRejected
方法,如下所示。
1
2
3
4
5extension Promise {
func `catch`(onError: @escaping (Error) -> Void) -> Promise<Void> {
return then(onFulfilled: { _ in }, onRejected: onError)
}
}
done
方法仅用于处理返回值,其可通过 then
方法实现,关键是实现 onFulfilled
方法,如下所示。
1
2
3
4
5extension Promise {
func done(onNext: @escaping (T) -> Void) -> Promise<Void> {
return then(onFulfilled: onNext)
}
}
finally
方法用于 Promise
链式调用的末尾,其并不接收之前任务的返回值和错误,支持用户在任务结束时执行状态无关的任务,具体实现如下所示。
1
2
3
4
5extension Promise {
func finally(onCompleted: @escaping () -> Void) -> Void {
then(onFulfilled: { _ in onCompleted() }, onRejected: { _ in onCompleted() })
}
}
内存管理
类似 Rx,Promise 的内存管理十分巧妙,其核心原理是
通过闭包强引用对象。下面,我们来分别介绍一下 Functor
then
和 Monad then
的内存管理。
Functor then
如下所示,为 Functor then
方法产生的内存管理示意图。
在初始化 Promise 时,resolve
和 reject
方法必须强引用 Promise,否则等到异步任务执行完成时,Promise
早已释放,根本无法通过 Promise 执行回调任务。
当调用 Functor then
方法时,Promise
的两个回调任务列表将引用 then
方法所传入的两个闭包
onFulfilled
和 onRejected
,同时引用
then
方法内部创建的 Promise 的 resolve
和
reject
方法。新创建的 Promise 又被自身的
resolve
和 reject
方法所引用,从而实现线性的内存引用关系。
Monad then
如下所示,为 Monad then
方法产生的内存管理示意图。
同样,当调用 Monad then
方法是,Promise
的两个回调任务数组将引用 then
方法所传入的两个闭包
onFulfilled
和 onRejected
,同时引用
then
方法内部创建的 Promise 的 reject
方法。从而实现线性的内存引用关系。
区别于 Functor then
,Monad then
方法的
onFulfilled
闭包会返回一个包装类型
Promise<R>
。因此,当 Promise 状态为
fulfilled
或 rejected
时,then
会立即返回由该闭包生成的 Promise;当 Promise 状态为 pending
时,then
会将闭包生成的 Promise 作为中间层
Promise,由中间层 Promise 调用 Functor
then
,从而产生一个间接的线性内存引用。
功能测试
下面,我们来编写一个网络请求的例子来对我们实现的 Promise 进行测试。
1 | enum NetworkError: Error { |
我们定义了一个 TestAPI
的类,其提供两个方法,分别请求用户信息和头像信息,返回值均为
Promise。其内部我们使用 GDC
延迟进行模拟,使用随机数设置网络请求的成功和失败情况。
接下来,我们来进行功能测试,依次请求用户信息和头像信息,如下所示。
1
2
3
4
5
6
7
8
9
10
11let api = TestAPI()
firstly {
api.user()
}.then { user in
print("user name => \(user)")
api.avatar()
}.catch { _ in
print("request error")
}.finally {
print("request complete")
}
当网络请求成功时,我们会得到如下内容: 1
2
3
4request user info
user name => User(name: "chuquan", avatarURL: "avatarurl")
request avatar info
request complete1
2
3request user info
request error
request complete
从执行顺序和结果而言,是符合我们的预期的。当然,我们还可以编写更多测试用例来进行测试,本文将不再赘述。
总结
本文,我们介绍了一种常见的异步编程技术 Promise,深入分析其设计原理,并最终手动实现一套简易的 Promise 框架。此外,我们还对 Promise 的内存管理进行了简要的分析,以深入了解内部的运行机制。
后续,有机会的话,我们来分析一款流行的 Promise 开源框架,以进一步验证 Promise 的设计。