Promise
Swift的Promise库,部分基于JavaScript的A+规范。
什么是Promise?
Promise是一种在将来某个时间点将存在(或者失败并报错)的值的表示方式。这与如何用Optional
表示一个可能或可能不存在的值相似。
使用特殊类型来表示将存在的values意味着,这些值可以被组合,以系统化的方式进行转换和构建。如果系统知道成功和失败是什么样子,构建那些异步操作就会变得容易得多。例如,它可以非常容易地编写可重用的代码,该代码可以
- 执行一系列依赖于对方的异步操作,并在最后一处完成
- 同时执行许多独立的异步操作,并使用一个完成块
- 比较许多异步操作,并返回最先完成的值
- 重试异步操作
- 给异步操作添加超时
Promises非常适合任何成功或失败只能发生一次的异步操作,如HTTP请求。如果有一个异步操作可能会"成功"多次,或者随着时间的推移传递一系列的值而不是单个值,那么可以看一下Signals或Observables。
基本用法
当值到达后,要访问该值,您需要使用一个代码块调用 then
方法。
let usersPromise = fetchUsers() // Promise<[User]>
usersPromise.then({ users in
self.users = users
})
users
Promise 中对数据的所有使用都通过 then
方法控制。
除了执行副作用(如将 users
实例变量设置在 self
上)之外,then
还允许您做两件事。首先,您可以转换 Promise 的内容,其次,您可以启动另一个 Promise 进行更多异步工作。要执行这些操作之一,从您传递给 then
的代码块中返回一些内容。每次调用 then
时,现有的 Promise 都会返回一个新的 Promise。
let usersPromise = fetchUsers() // Promise<[User]>
let firstUserPromise = usersPromise.then({ users in // Promise<User>
return users[0]
})
let followersPromise = firstUserPromise.then({ firstUser in //Promise<[Follower]>
return fetchFollowers(of: firstUser)
})
followersPromise.then({ followers in
self.followers = followers
})
根据您返回的是普通值还是 Promise,Promise
将确定是否转换内部内容,或者触发下一个 Promise 并等待其结果。
只要您传递给 then
的代码块只有一行长,其类型签名就会被推断出来,这将使 Promise 的阅读和操作变得更容易。
由于每个 then
调用都返回一个新的 Promise
,因此您可以将它们写成一条长链。上面的代码作为一个链,应该写成
fetchUsers()
.then({ users in
return users[0]
})
.then({ firstUser in
return fetchFollowers(of: firstUser)
})
.then({ followers in
self.followers = followers
})
要捕获过程中产生的任何错误,您还可以添加一个 catch
块。
fetchUsers()
.then({ users in
return users[0]
})
.then({ firstUser in
return fetchFollowers(of: firstUser)
})
.then({ followers in
self.followers = followers
})
.catch({ error in
displayError(error)
})
如果链中的任何步骤失败,则不会执行更多的 then
块。只有失败块会被执行。这一点也在类型系统中强制执行。如果 fetchUsers()
Promise 失败(例如,由于缺少互联网),则没有方法为 users
变量构造一个有效的值,并且该块不会被执行。
创建 Promises
要创建一个 Promise,有一个便利的初始化器可以接受一个代码块,并提供 fulfill
或 reject
Promise 的函数
let promise = Promise<Data>(work: { fulfill, reject in
try fulfill(Data(contentsOf: someURL)
})
它将自动在全局后台线程上运行。
您可以使用这种初始化器来包装基于完成块的 API,如 URLSession
。
let promise = Promise<(Data, HTTPURLResponse)>(work: { fulfill, reject in
self.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
reject(error)
} else if let data = data, let response = response as? HTTPURLResponse {
fulfill((data, response))
} else {
fatalError("Something has gone horribly wrong.")
}
}).resume()
})
如果您正在包装的 API 对它在哪个线程上运行敏感(例如任何 UIKit 代码),请确保向 work:
初始化器传递含有 queue: .main
参数的 queue:
,它将在主队列上执行。
对于基于代理的 API,您可以使用默认的初始化器创建处于 .pending
状态的 Promise。
let promise = Promise()
然后使用 fulfill
和 reject
实例方法来更改其状态。
高级使用
由于 Promise 将成功和失败块的格式正式化,因此可以在其上构建行为。
总是
例如,如果你想在承诺成功或失败时执行代码,可以使用 总是
。
activityIndicator.startAnimating()
fetchUsers()
.then({ users in
self.users = users
})
.always({
self.activityIndicator.stopAnimating()
})
即使网络请求失败,活动指示器也会停止。请注意,传递给 总是
的代码块没有参数。因为Promise不知道它是否会成功或失败,所以它既不会给你值,也不会给你错误。
确保
确保
是一个接受一个谓词的方法,如果该谓词失败,它将拒绝承诺链。
URLSession.shared.dataTask(with: request)
.ensure({ data, httpResponse in
return httpResponse.statusCode == 200
})
.then({ data, httpResponse in
// the statusCode is valid
})
.catch({ error in
// the network request failed, or the status code was invalid
})
静态方法
例如,像 zip
、race
、retry
、all
、kickoff
这样的静态方法存在于一个名为 Promises
的命名空间中。之前,它们是 Promise
类中的静态函数,但这意味着你必须使用通用的类型来专门化它们,然后才能使用,例如 Promise<()>.all
。这很丑陋且难以输入,所以从v2.0开始,现在你可以写 Promises.all
。
all
Promises.all
是一个静态方法,它等待您给出的所有承诺都满足,一旦它们都满足了,它就使用所有满足值的数组自行满足。例如,您可能想为数组中的每个项目编写一次访问API端点的代码。`map` 和 `Promises.all` 使这变得非常简单
let userPromises = users.map({ user in
APIClient.followUser(user)
})
Promises.all(userPromises)
.then({
//all the users are now followed!
})
.catch ({ error in
//one of the API requests failed
})
kickoff
由于promise的then块是函数可以抛出异常的安全空间,即使没有异步工作要做,有时候进入这些安全空间也是有用的。Promises.kickoff
就是为了这一点而设计的。
Promises
.kickoff({
return try initialValueFromThrowingFunction()
})
.then({ value in
//use the value from the throwing function
})
.catch({ error in
//if there was an error, you can handle it here.
})
这(结合Optional.unwrap()
)在你想从一个可选值启动promise链时尤其有用。
其他行为
这些都是最有用的行为,但也有其他行为,如race
(与多个promise竞争),retry
(允许你多次重试单个promise)和recover
(允许你在一个错误的情况下返回一个新的Promise
,允许你从失败中恢复),以及其他。
你可以在这些行为中在Promises+Extras.swift文件中找到。
可验证队列
Promise上每个接受一个块的挂载点都接受一个参数名为on:
的执行上下文。通常,这个执行上下文是一个队列。
Promise<Void>(queue: .main, work: { fulfill, reject in
viewController.present(viewControllerToPresent, animated: flag, completion: {
fulfill()
})
}).then(on: DispatchQueue.global(), {
return try Data(contentsOf: someURL)
})
由于ExecutionContext
是一个协议,其他的东西也可以传递到这里。其中一个特别有用的是InvalidatableQueue
。当与表单元格一起工作时,通常需要忽略promise的结果。为此,每个单元格可以保留一个InvalidatableQueue
。一个InvalidatableQueue
是一个可以被无效化的执行上下文。如果上下文被无效化,那么传递给它的块将被丢弃而不执行。
为了与表单元格一起使用,队列应在prepareForReuse()
期间被无效化和重置。
class SomeTableViewCell: UITableViewCell {
var invalidatableQueue = InvalidatableQueue()
func showImage(at url: URL) {
ImageFetcher(url)
.fetch()
.then(on: invalidatableQueue, { image in
self.imageView.image = image
})
}
override func prepareForReuse() {
super.prepareForReuse()
invalidatableQueue.invalidate()
invalidatableQueue = InvalidatableQueue()
}
}
警告:不要在正在在可验证队列上执行的东西上链式处理块。then
块返回)不会停止链式处理,但返回值或promise的
then
块会停止链式处理。由于块无法执行,链中下一个值的计算结果将不可知,下一个promise将永久处于pending
状态,阻止释放资源。
易于使用
在编写这个Promise
库时,我做出了几个设计决策,旨在使他们尽可能容易使用。
简化命名
其他诺言库使用函数名称来命名then
,例如map
和flatMap
。使用这些单子功能术语的好处很小,但在理解上的成本却很高。在这个库中,您调用then
,并返回您需要的任何内容,该库会找出如何处理它。
错误参数化
其他诺言库允许您定义每个诺言将返回的错误类型。从理论上讲,这是一个有用的特性,允许您知道错误将是什么类型,在catch
块中。
在实践中,这将变得很压制。在实践中,如果您使用来自两个不同领域的错误,您必须使用以下选项之一:a)使用最低公共分母的错误,如NSError
,或 b)调用如mapError
的函数将错误从一个领域转换为另一个领域。
请注意,Swift的内置错误处理系统没有类型错误,而是选择使用模式匹配。
抛出
最后,您可以在所有块中使用try
和throw
,并且库会自动将其转换为诺言拒绝。这使得处理抛出错误的API变得更加容易。为了扩展我们的URLSession
示例,我们可以轻松使用抛出JSONSerialization API。
URLSession.shared.dataTask(with: request)
.ensure({ data, httpResponse in httpResponse.statusCode == 200 })
.then({ data, httpResponse in
return try JSONSerialization.jsonObject(with: data)
})
.then({ json in
// use the json
})
通过一点扩展,可以简化处理可选的操作。
struct NilError: Error { }
extension Optional {
func unwrap() throws -> Wrapped {
guard let result = self else {
throw NilError()
}
return result
}
}
因为您在一个可以自由抛出且将为您处理(以拒绝的形式呈现)的环境中,您现在可以轻松地展开可选的。例如,如果您需要从JSON字典中获取特定的键
.then({ json in
return try (json["user"] as? [String: Any]).unwrap()
})
并将您的可选转换为非可选。
线程模型
这个库的线程模型非常简单。默认情况下,init(work:)
在后台队列中执行,其他基于块的方法(如then
、catch
、always
等)在主线程上执行。可以通过传入第一个参数的DispatchQueue
对象来覆盖这些。
Promise<Void>(work: { fulfill, reject in
viewController.present(viewControllerToPresent, animated: flag, completion: {
fulfill()
})
}).then(on: DispatchQueue.global(), {
return try Data(contentsOf: someURL)
}).then(on: DispatchQueue.main, {
self.data = data
})
安装
CocoaPods
-
将以下内容添加到您的Podfile中。
pod 'Promises'
-
使用框架集成您的依赖项:将
use_frameworks!
添加到您的Podfile中。 -
运行
pod install
。
玩耍
要开始使用这个库玩耍,您可以使用附带的Promise.playground
。简单地在Xcode中打开.xcodeproj
,构建方案,然后从项目中打开playground(开始玩耍)。