PromiseLite
让您连锁异步和同步函数
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 ... }
,在初始化承诺对象的过程中由初始化器立即执行。 - 第一个达到的
resolve
、reject
或throw
的 获胜,任何进一步的调用都将被 忽略。
链式调用
使用 map
和 flatMap
来链式调用承诺。
提示:使函数尽可能小巧,以便容易组合。例如
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
等...
处理错误
错误会一直传播,直到被 catch
或 flatCatch
捕获。一旦捕获,链式调用将恢复并继续。
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文件。