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,其定义了一个未来值。
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 支持以同步代码结构编写异步代码逻辑,其提供一系列便利方法以支持链式调用,如: then
、 done
、 catch
、 finally
等。注意,不同的编程语言或库实现中,方法命名有所不同。
如下所示,是一个以 JavaScript 编写的 Promise 的基本用法。
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
是起始状态, fulfilled
和 rejected
是结束状态。一旦 Promise 的状态发生了变化,它将不会再改变。因此,Promise 是一种 单赋值 的结构。
Promise 内部的状态由 执行器(executor) 或 解析器(resolver) 来进行更新。Promise 创建时的状态默认为 pending
,用户为 Promise 提供状态转移逻辑,比如:网络请求成功时将状态设置为 fulfilled
,网络请求失败时将状态设置为 rejected
。通常,执行器会提供两个方法 resolve
和 reject
分别用于设置 fulfilled
和 rejected
状态。
此外,Promise 还支持通过链式操作符实现回调任务的链式执行,其原理是在内部维护一个回调任务列表,当 Promise 到达结束状态时,自动执行内部的回调任务,从而整体实现异步任务的链式执行。
核心实现
下面,我们来手动实现 Promise 的核心逻辑,编程语言为 Swift。
状态
首先,定义 Promise 的三个状态,如下所示。
enum State { case pending case fulfilled case rejected }
执行器
Promise 的核心目标是为了解决异步(或同步)任务的相关问题。首先,要解决两个问题:
- 如何表示异步任务?
- 如何更新任务状态?
对于第一个问题,很简单,我们可以提供一个闭包,让用户在闭包中自定义任务即可。
对于第二个问题,同样,我们可以提供两个状态更新方法,让用户在任务的特定阶段调用即可。
这里,我们定义的执行器如下所示。
class 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
,具体定义如下所示。
class Promise<T> { ... private(set) var state: State = .pending private(set) var value: T? private(set) var error: Error? }
链式执行
Promise 的核心功能之一是 链式执行 异步任务。那么,如何实现链式执行异步任务呢?很简单,我们将后一个 Promise 的异步任务存储在前一个 Promise 的回调任务列表中,当前一个 Promise 达到结束状态( fulfilled
或 rejected
)时,执行其内部保存的下一个(组)回调任务即可。
对此,我们可以在 Promise 内部保存两个数组,分别用户存储 fulfilled
状态和 rejected
状态时要执行的回调任务。除此之外,我们还需要对 resolve
和 reject
方法进行进一步加工,方法调用时,分别设置当前异步任务的返回值、状态,并执行回调任务。具体定义如下所示。
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) } } }
可以看到,我们分别使用 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
方法的具体实现如下所示。
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) } } } }
注意, onFulfilled
和 onRejected
闭包的入参和返回值,这是 then
能够实现异步任务的值传递的关键。
Monad
上一节的 then
方法主要是 Functor 实现,为了进一步扩展 then
方法的,我们来实现 Monad then
方法,具体实现如下所示。
关于 Functor 和 Monad 的概念,可以阅读 《函数式编程——Functor、Applicative、Monad》 。
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 还具有一系列遍历方法,如: fistly
、 catch
、 done
、 finally
等。下面,我们依次实现。
firstly
方法本质上是语法糖,表示异步任务组的第一步。我们实现一个全局方法,通过闭包实现任务的具体逻辑,如下所示。
func firstly<T>(closure: @escaping () -> Promise<T>) -> Promise<T> { return closure() }
catch
方法仅用于处理错误,其可通过 then
方法实现,关键是实现 onRejected
方法,如下所示。
extension Promise { func `catch`(onError: @escaping (Error) -> Void) -> Promise<Void> { return then(onFulfilled: { _ in }, onRejected: onError) } }
done
方法仅用于处理返回值,其可通过 then
方法实现,关键是实现 onFulfilled
方法,如下所示。
extension Promise { func done(onNext: @escaping (T) -> Void) -> Promise<Void> { return then(onFulfilled: onNext) } }
finally
方法用于 Promise 链式调用的末尾,其并不接收之前任务的返回值和错误,支持用户在任务结束时执行状态无关的任务,具体实现如下所示。
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 时, 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 进行测试。
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 延迟进行模拟,使用随机数设置网络请求的成功和失败情况。
接下来,我们来进行功能测试,依次请求用户信息和头像信息,如下所示。
let api = TestAPI() firstly { api.user() }.then { user in print("user name => \(user)") api.avatar() }.catch { _ in print("request error") }.finally { print("request complete") }
当网络请求成功时,我们会得到如下内容:
request user info user name => User(name: "chuquan", avatarURL: "avatarurl") request avatar info request complete
当网络请求失败时,我们则得到如下内容:
request user info request error request complete
从执行顺序和结果而言,是符合我们的预期的。当然,我们还可以编写更多测试用例来进行测试,本文将不再赘述。
总结
本文,我们介绍了一种常见的异步编程技术 Promise,深入分析其设计原理,并最终手动实现一套简易的 Promise 框架。此外,我们还对 Promise 的内存管理进行了简要的分析,以深入了解内部的运行机制。
后续,有机会的话,我们来分析一款流行的 Promise 开源框架,以进一步验证 Promise 的设计。
参考
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论