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
2
3
4
5
6
7
8
9
Future<UserInfo> getUserInfo(BuildContext context) async {
try {
final response = await get('https://chuquan.me/userinfo');
final userInfo = UserInfo.fromJson(response.data as Map<String, dynamic>);
return userInfo;
} on DioError catch (e) {
Toast.instance.showNetworkError(context, e);
}
}

Future 和 Promise 来源于函数式编程语言,其目的是分离一个值和生产值的方法,从而简化异步编程。本质上,两者是一一对应的。

很多语言都有 Future 和 Promise 的实现,比如:Swift Task、C# Task、C++ std::future、Scala Future 对应的是 Future 的实现;C++ std::promise、JavaScript Promise、Scala Promise 对应的是 Promise 的实现。

基本用法

Promise 支持以同步代码结构编写异步代码逻辑,其提供一系列便利方法以支持链式调用,如:thendonecatchfinally 等。注意,不同的编程语言或库实现中,方法命名有所不同。

如下所示,是一个以 JavaScript 编写的 Promise 的基本用法。

1
2
3
4
5
6
7
8
9
getJSON("/post/1.json")
.then(function(post) {
return getJSON(post.commentURL);
})
.then(function (comments) {
console.log("resolved: ", comments);
}, function (err){
console.log("rejected: ", err);
});

基本原理

本质上,Promise 是一个对象,其包含三种状态,分别是:

  • pending:表示进行中状态
  • fulfilled:表示已成功状态状态。此时,Promise 得到一个结果值 value
  • rejected:表示已失败状态。此时,Promise 得到一个错误值 error,用于表示错误原因。

pending 是起始状态,fulfilledrejected 是结束状态。一旦 Promise 的状态发生了变化,它将不会再改变。因此,Promise 是一种 单赋值 的结构。

Promise 内部的状态由 执行器(executor)解析器(resolver) 来进行更新。Promise 创建时的状态默认为 pending,用户为 Promise 提供状态转移逻辑,比如:网络请求成功时将状态设置为 fulfilled,网络请求失败时将状态设置为 rejected。通常,执行器会提供两个方法 resolvereject 分别用于设置 fulfilledrejected 状态。

此外,Promise 还支持通过链式操作符实现回调任务的链式执行,其原理是在内部维护一个回调任务列表,当 Promise 到达结束状态时,自动执行内部的回调任务,从而整体实现异步任务的链式执行。

核心实现

下面,我们来手动实现 Promise 的核心逻辑,编程语言为 Swift。

状态

首先,定义 Promise 的三个状态,如下所示。

1
2
3
4
5
enum State {
case pending
case fulfilled
case rejected
}

执行器

Promise 的核心目标是为了解决异步(或同步)任务的相关问题。首先,要解决两个问题:

  • 如何表示异步任务?
  • 如何更新任务状态?

对于第一个问题,很简单,我们可以提供一个闭包,让用户在闭包中自定义任务即可。

对于第二个问题,同样,我们可以提供两个状态更新方法,让用户在任务的特定阶段调用即可。

这里,我们定义的执行器如下所示。

1
2
3
4
5
class Promise<T> {
typealias Resolve<T> = (T) -> Void
typealias Reject = (Error) -> Void
typealias Executor = (_ resolve: @escaping Resolve<T>, _ reject: @escaping Reject) -> Void
}

可以看到,上述定义的执行器是一个闭包,闭包的参数是两个状态更新方法,分别是 resolvereject,可供用户在任务的特定阶段调用,以更新任务的状态。

由于 resolvereject 方法分别用于设置 fulfilledrejected 状态,两个状态分别对应两个值:valueerror,从方法的入参可以看出两者的区别。因此,除了状态之外,还需定义两个字段,分别用于保存 valueerror,具体定义如下所示。

1
2
3
4
5
6
7
class Promise<T> {
...

private(set) var state: State = .pending
private(set) var value: T?
private(set) var error: Error?
}

链式执行

Promise 的核心功能之一是 链式执行 异步任务。那么,如何实现链式执行异步任务呢?很简单,我们将后一个 Promise 的异步任务存储在前一个 Promise 的回调任务列表中,当前一个 Promise 达到结束状态(fulfilledrejected)时,执行其内部保存的下一个(组)回调任务即可。

对此,我们可以在 Promise 内部保存两个数组,分别用户存储 fulfilled 状态和 rejected 状态时要执行的回调任务。除此之外,我们还需要对 resolvereject 方法进行进一步加工,方法调用时,分别设置当前异步任务的返回值、状态,并执行回调任务。具体定义如下所示。

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
class Promise<T> {
...

private(set) var onFulfilledCallbacks = [Resolve<T>]()
private(set) var onRejectedCallbacks = [Reject]()

init(_ executor: Executor) {
// 注意:resolve 和 reject 必须强引用 self,避免在执行 resolve 和 reject 之前系统释放 self
let resolve: Resolve<T> = { value in
self.value = value
self.onFulfilledCallbacks.forEach { onFullfilled in
onFullfilled(value)
}
self.state = .fulfilled
}
let reject: Reject = { error in
self.error = error
self.onRejectedCallbacks.forEach { onRejected in
onRejected(error)
}
self.state = .rejected
}
executor { value in
resolve(value)
} _: { error in
reject(error)
}
}
}

可以看到,我们分别使用 onFulfilledCallbacksonRejectedCallbacks 保存回调任务。同时定义了 resolvereject 两个方法,内部分别设置异步任务的返回值、状态,并执行回调任务。

Promise 初始化时,执行器会立即执行,从而触发异步任务的执行,同时将两个状态更新方法作为参数传入闭包,以供用户在任务的特定阶段调用。

任务串联

Promise 通过 then 方法来串联任务,即让前一个 Promise 保存下一个 Promise 的任务。then 方法包含两个闭包 onFulfilledonRejected,分别表示不同状态的回调任务,其在前一个 Promise 的状态为 fulfilledrejected 时分别执行。

then 串联任务时,我们需要考虑前一个 Promise 的状态。这里,我们分三种情况进行考虑:

  • 当前一个 Promise 的状态为 pending 时,我们创建一个 Promise,其任务的核心是将 onFulfilledonRejected 分别加入前一个 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
37
extension 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)
}

}
}
}

注意,onFulfilledonRejected 闭包的入参和返回值,这是 then 能够实现异步任务的值传递的关键。

Monad

上一节的 then 方法主要是 Functor 实现,为了进一步扩展 then 方法的,我们来实现 Monad then 方法,具体实现如下所示。

关于 Functor 和 Monad 的概念,可以阅读 《函数式编程——Functor、Applicative、Monad》

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
extension Promise {
// Monad
@discardableResult
func then<R>(onFulfilled: @escaping (T) -> Promise<R>, onRejected: @escaping (Error) -> Void) -> Promise<R> {
switch state {
case .pending:
return Promise<R> { [weak self] resolve, reject in
// 初始化时即执行
// 在 prev promise 的 callback 队列加入一个生成 midd promise 的任务。
// 在 midd promise 的 callback 队列加入一个任务,修改 curr promise 状态。
self?.onFulfilledCallbacks.append { value in
let promise = onFulfilled(value)
promise.then(onFulfilled: { r in
resolve(r)
}, onRejected: { _ in })
}
self?.onRejectedCallbacks.append { error in
onRejected(error)
reject(error)
}
}
case .fulfilled:
return onFulfilled(value!)
case .rejected:
return Promise<R> { _, reject in
onRejected(error!)
reject(error!)
}
}
}
}

便利方法

通常 Promise 还具有一系列遍历方法,如:fistlycatchdonefinally 等。下面,我们依次实现。

firstly 方法本质上是语法糖,表示异步任务组的第一步。我们实现一个全局方法,通过闭包实现任务的具体逻辑,如下所示。

1
2
3
func firstly<T>(closure: @escaping () -> Promise<T>) -> Promise<T> {
return closure()
}

catch 方法仅用于处理错误,其可通过 then 方法实现,关键是实现 onRejected 方法,如下所示。

1
2
3
4
5
extension Promise {
func `catch`(onError: @escaping (Error) -> Void) -> Promise<Void> {
return then(onFulfilled: { _ in }, onRejected: onError)
}
}

done 方法仅用于处理返回值,其可通过 then 方法实现,关键是实现 onFulfilled 方法,如下所示。

1
2
3
4
5
extension Promise {
func done(onNext: @escaping (T) -> Void) -> Promise<Void> {
return then(onFulfilled: onNext)
}
}

finally 方法用于 Promise 链式调用的末尾,其并不接收之前任务的返回值和错误,支持用户在任务结束时执行状态无关的任务,具体实现如下所示。

1
2
3
4
5
extension 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 时,resolvereject 方法必须强引用 Promise,否则等到异步任务执行完成时,Promise 早已释放,根本无法通过 Promise 执行回调任务。

当调用 Functor then 方法时,Promise 的两个回调任务列表将引用 then 方法所传入的两个闭包 onFulfilledonRejected,同时引用 then 方法内部创建的 Promise 的 resolvereject 方法。新创建的 Promise 又被自身的 resolvereject 方法所引用,从而实现线性的内存引用关系。

Monad then

如下所示,为 Monad then 方法产生的内存管理示意图。

同样,当调用 Monad then 方法是,Promise 的两个回调任务数组将引用 then 方法所传入的两个闭包 onFulfilledonRejected,同时引用 then 方法内部创建的 Promise 的 reject 方法。从而实现线性的内存引用关系。

区别于 Functor then,Monad then 方法的 onFulfilled 闭包会返回一个包装类型 Promise<R>。因此,当 Promise 状态为 fulfilledrejected 时,then 会立即返回由该闭包生成的 Promise;当 Promise 状态为 pending 时,then 会将闭包生成的 Promise 作为中间层 Promise,由中间层 Promise 调用 Functor then,从而产生一个间接的线性内存引用。

功能测试

下面,我们来编写一个网络请求的例子来对我们实现的 Promise 进行测试。

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
37
38
39
40
41
42
43
44
45
enum NetworkError: Error {
case decodeError
case responseError
}

struct User {
let name: String
let avatarURL: String

var description: String { "name: => \(name); avatar => \(avatarURL)" }
}

class TestAPI {
func user() -> Promise<User> {
return Promise<User> { (resolve, reject) in
// Mock HTTP Request
print("request user info")
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
let result = arc4random() % 10 != 0
if result {
let user = User(name: "chuquan", avatarURL: "avatarurl")
resolve(user)
} else {
reject(NetworkError.responseError)
}
}
}
}

func avatar() -> Promise<UIImage> {
return Promise<UIImage> { (resolve, reject) in
// Mock HTTP Request
print("request avatar info")
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
let result = arc4random() % 10 != 0
if result {
let avatar = UIImage()
resolve(avatar)
} else {
reject(NetworkError.decodeError)
}
}
}
}
}

我们定义了一个 TestAPI 的类,其提供两个方法,分别请求用户信息和头像信息,返回值均为 Promise。其内部我们使用 GDC 延迟进行模拟,使用随机数设置网络请求的成功和失败情况。

接下来,我们来进行功能测试,依次请求用户信息和头像信息,如下所示。

1
2
3
4
5
6
7
8
9
10
11
let 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
4
request user info
user name => User(name: "chuquan", avatarURL: "avatarurl")
request avatar info
request complete
当网络请求失败时,我们则得到如下内容:
1
2
3
request user info
request error
request complete

从执行顺序和结果而言,是符合我们的预期的。当然,我们还可以编写更多测试用例来进行测试,本文将不再赘述。

总结

本文,我们介绍了一种常见的异步编程技术 Promise,深入分析其设计原理,并最终手动实现一套简易的 Promise 框架。此外,我们还对 Promise 的内存管理进行了简要的分析,以深入了解内部的运行机制。

后续,有机会的话,我们来分析一款流行的 Promise 开源框架,以进一步验证 Promise 的设计。

参考

  1. Futures and Promises
  2. Futures and Promises
  3. Promises/A+
  4. ECMAScript 6 入门
  5. 从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节
  6. Future与Promise
  7. A promise is a Monad
  8. async/await is just the do-notation of the Promise monad
  9. What The Heck Is A Monad
  10. Understand promises before you start using async/await