PromiseLite 2.0.0

PromiseLite 2.0.0

François Rouault 维护。



  • François Rouault

chaining

PromiseLite

让您连锁异步和同步函数

CI Status codecov Version License Platform

PromiseLite 是 Swift 中 JavaScript Promises 概念的一个实现。

它是纯 Swift,100% 经过测试,并且非常轻量,约 150 行代码。

安装

PromiseLite 通过 CocoaPods 提供。将以下行添加到您的 Podfile 中:

pod 'PromiseLite'

提示:在您的 AppDelegate.swift(或其它地方)中添加 typealias Promise = PromiseLite 行,这样会更短。在本页的其余部分,我假设您已经这样做了。

开始使用

在 5 分钟内开始使用承诺并在您的代码中连锁异步和同步函数。

假设您有一个以下函数,它使用完成块来处理异步操作。您可能会熟悉

func fetch(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
  URLSession.shared.dataTask(with: url) { data, response, error in
    if let data = data {
      completion(.success(data))
    } else {
      completion(.failure(AppError.noData))
    }
  }
}

为了能够连锁调用,您必须去除完成块。欢迎来到 plromises!

创建另一个返回获取 Data 的承诺的函数。在这个新函数中,您可以在 Promise 的闭包中调用先前定义的 fetch(url:completion:) 函数。

func fetch(url: URL) -> Promise<Data> {
  Promise { resolve, reject in
    fetch(url: url) { result in
      switch result {
      case .success(let data): resolve(data)
      case .failure(let error): reject(error)
      }
    }
  }
}

现在您可以使用 flatMap 连锁承诺,并使用 map 将承诺连接到常规函数。

let url = URL(string: "https://your.endpoint.com/user/36")!

fetch(url: url)
  .map     { try JSONDecoder().decode(User.self, from: $0) }
  .map     { $0.age >= 18 ? $0 : try { throw AppError.userIsMinor }() }
  .flatMap { fetchContents(user: $0) }
  .map     { display(contents: $0) }
  .catch   { display(error: $0) }

就是这样! 🎯

为了比较,上面的连接相当于是以下使用完成块的代码🤯

// fetch(url: url) { result in
//   switch result {
//   case .success(let data):
//     do {
//       let user = try JSONDecoder().decode(User.self, from: data)
//
//       guard user.age >= 18 else {
//        display(error: AppError.userIsMinor)
//        return
//       }
//
//       fetchContents(user: user) { result2 in
//         switch result2:
//         case .success(let contents): display(contents: contents)
//         case .failure(let error): display(error: error)
//       }
//     } catch {
//       display(error: error)
//     }
//   case .failure(let error):
//     display(error: error)
//   }
// }

承诺

什么是承诺?

承诺代表了操作(异步或同步)的最终结果。

其初始化参数称为 执行器。它是一个闭包,接受两个函数作为参数

  • resolve:一个接收一个值参数(承诺的结果)的函数
  • reject:一个接收一个错误参数的函数

例如,我们可以这样定义一个承诺

func divide(a: Double, by b: Double) -> Promise<Double> {
  let executor: ((Double) -> Void, (Error) -> Void) -> Void = { resolve, reject in
    b != 0
      ? resolve(a / b)
      : reject(AppError.cannotDivideByZero)
  }
  return Promise<Double>(executor)
}

幸运的是,Swift提供了一些快捷的语法和类型推断。因此,前面的代码可以简化如下

func divide(a: Double, by b: Double) -> Promise<Double> {
  Promise { resolve, reject in
    b != 0
      ? resolve(a / b)
      : reject(AppError.cannotDivideByZero)
  }
}

更多示例...

下面是一个同步函数的示例,该函数接收一个字符串参数并返回一个URL的承诺。

func url(from urlStr: String) -> Promise<URL> {
  Promise { resolve, reject in
    if let url = URL(string: urlStr) {
      resolve(url) // ✅ the url string is valid, call `resolve`
    } else {
      reject(AppError.invalidUrl) // ❌ the url string is not valid, call `reject`
    }
  }
}

下面是将 dataTask 包裹在一个承诺中以检索 Data 的建议。

func fetch(url: URL) -> Promise<Data> {
  Promise { resolve, reject in
    URLSession.shared.dataTask(with: url) { data, response, error in
      if let error = error {
        reject(error) // ❌ an error occured, call `reject` or `throw`
        return
      }

      guard let data = data else {
        throw AppError.noData // ❌ could not retrieve data, call `reject` or `throw`
        return
      }

      resolve(data) // ✅ data retrieved, call `resolve`
    }
  }
}

辅助工具

Promise.resolve("foo") // is equivalent to `Promise { resolve, _ in resolve("foo") }`
Promise<String>.reject(AppError.💥) // is equivalent to `Promise<String> { _, reject in reject(AppError.💥) }`

// Note that, in this situation, you must specify the type `<String>` because there is nothing in the executor that can help Swift guess the type.

知识要点

  • 执行器函数,即 { resolve, reject in ... },在初始化承诺对象的过程中由初始化器立即执行。
  • 第一个达到的 resolverejectthrow获胜,任何进一步的调用都将被 忽略

链式调用

使用 mapflatMap 来链式调用承诺。

提示:使函数尽可能小巧,以便容易组合。例如

Promise.resolve("https://your.endpoint.com/user/\(id)")
  .flatMap { url(from: $0) }
  .flatMap { fetch(url: $0) }
  .map     { try JSONDecoder().decode(User.self, from: $0) }
  .map     { $0.age >= 18 }
  .flatMap { $0 ? fetchContents() : Promise.reject(AppError.userIsUnderage) }
  .map     { display(contents: $0) }

在上面的示例中,我们从一个字符串 https://your.endpoint.com/user/\(id) 开始,然后调用 url(from:)string 转换为 URL 等...

处理错误

错误会一直传播,直到被 catchflatCatch 捕获。一旦捕获,链式调用将恢复并继续。

Promise.resolve("not://a.va|id.url")
  .flatMap { url(from: $0) } // 💥 this promise rejects because the url is invalid
  .flatMap { /* not reached */ }
  .map     { /* not reached */ }
  .map     { /* not reached */ }
  .flatMap { /* not reached */ }
  .map     { /* not reached */ }
  .catch   { /* REACHED! */ }
  .map     { /* REACHED! */ }
  ...

如何调试链式调用?

通过设置 PromiseLiteConfiguration.debugger 实例来监视承诺的生命周期。在承诺开始时和它解析或拒绝时都会调用此实例。PromiseLite 提供了 PromiseLiteDebugger 协议的默认实现:DefaultPromiseLiteDebugger(output:)

// Do the following to print default debugger output in the console.
PromiseLiteConfiguration.debugger = DefaultPromiseLiteDebugger { print($0) }

此外,承诺可以初始化为包含描述,以便更容易理解当前正在执行哪个承诺。默认情况下,承诺的描述为 PromiseLite<TheType>

func fetchUser(id: String) -> PromiseLite<User> {
  PromiseLite<User>("fetch user") { resolve, reject in
    ...
  }
}

func saveInDatabase(user: User) -> PromiseLite<Bool> {
  PromiseLite<Bool>("save in db") { resolve, reject in
    ...
  }
}

fetchUser(id: "123")
  .flatMap { saveInDatabase(user: $0) }
  .map { [weak self] _ in self?.updateUI() }
  .catch { [weak self] err in self?.updateUI(error: err) }

// The above chaining will result in the following logs in the console:
// 🔗 | fetch user resolves ✅ in 1.36 sec
// 🔗 | save in db resolves ✅ in 0.72 sec
// 🔗 | PromiseLite<()> resolves ✅ in 0.00 sec
// 🔗 | PromiseLite<()> resolves ✅ in 0.00 sec
// Note that `map` and `catch` implicitly creates a promise with the default description. Since `updateUI` is a function that returns void, the type's value of the implicity created promise is `()`.
// Note that `catch` actually resolves because it implicitly creates a promise that resolves regardless of whether the previous promise resolved or rejected.

// In case, `fetchUser(id:)` would reject, the above chaining would result in the following logs in the console:
// 🔗 | fetch user rejects ❌ in 1.36 sec
// 🔗 | save in db rejects ❌ in 0.00 sec
// 🔗 | PromiseLite<()> rejects ❌ in 0.00 sec
// 🔗 | PromiseLite<()> resolves ✅ in 0.00 sec
// Note that rejection does propagate until `catch` handle the error returning a promise that resolves.

变更日志

访问 CHANGELOG.md

作者

  • François Rouault

随时提交合并请求。

许可证

PromisELite遵循MIT许可证。有关更多信息,请参阅LICENSE文件。