Promise 核心实现原理

发布于 2023-11-07 12:41:03 字数 14579 浏览 46 评论 0

在传统的基于 闭包 的异步编程中,经常会出现 地狱嵌套 的问题,这使得高度异步的代码几乎无法阅读。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 是一种 单赋值 的结构。

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

在初始化 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 进行测试。

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

参考

  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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

眼眸印温柔

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

内心激荡

文章 0 评论 0

JSmiles

文章 0 评论 0

左秋

文章 0 评论 0

迪街小绵羊

文章 0 评论 0

瞳孔里扚悲伤

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文