2

Promise 核心实现原理

 1 year ago
source link: http://chuquan.me/2022/10/16/promise-core-implement/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在传统的基于 闭包 的异步编程中,经常会出现 地狱嵌套 的问题,这使得高度异步的代码几乎无法阅读。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 支持以同步代码结构编写异步代码逻辑,其提供一系列便利方法以支持链式调用,如:thendonecatchfinally 等。注意,不同的编程语言或库实现中,方法命名有所不同。

如下所示,是一个以 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 是起始状态,fulfilledrejected 是结束状态。一旦 Promise 的状态发生了变化,它将不会再改变。因此,Promise 是一种 单赋值 的结构。

resize,w_800

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

此外,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
}

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

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

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 方法进行进一步加工,方法调用时,分别设置当前异步任务的返回值、状态,并执行回调任务。具体定义如下所示。

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 方法的具体实现如下所示。

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》

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

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 方法产生的内存管理示意图。

resize,w_800

在初始化 Promise 时,resolvereject 方法必须强引用 Promise,否则等到异步任务执行完成时,Promise 早已释放,根本无法通过 Promise 执行回调任务。

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

Monad then

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

resize,w_800

同样,当调用 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 进行测试。

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 的设计。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK