Swift 快速、强大且轻量级的 Promise 实现。
如果 Promise 闭包在同一个队列上,则会同步地一个接一个地调用,否则会异步调用。
整个实现由数百行代码组成。
PromiseQ 基于 struct
和回调堆栈,可以避免许多内存管理问题,例如循环引用等。
基于 JavaScript Promises/A+ 规范,支持 async/await
,并且还包括标准方法:Promise.all
、Promise.race
、Promise.any
、Promise.resolve/reject
。
这是一个额外的有用功能,可以暂停
Promise 的执行,并在以后恢复
它们。 暂停不会影响已经开始执行的 Promise,它会停止下一个 Promise 的执行。
可以取消
所有排队的 Promise,以停止异步逻辑。 取消不会影响已经开始执行的 Promise,它会取消 Promise 并停止链中下一个 Promise 的执行。
Promise 是一种泛型类型,表示异步操作,您可以使用闭包以简单的方式创建它,例如:
Promise {
try String(contentsOfFile: file)
}
提供的闭包在创建 Promise 后异步调用。 默认情况下,闭包在全局默认队列 DispatchQueue.global()
上运行,但您也可以指定需要运行的队列。
Promise(.main) {
self.label.text = try String(contentsOfFile: file) // Runs on the main queue
}
当闭包返回一个值时,Promise 可以被解析 (resolve);当闭包抛出一个错误时,Promise 可以被拒绝 (reject)。
此外,闭包可以使用 resolve/reject
回调来解决 Promise 以进行异步任务。
Promise { resolve, reject in
// Will be resolved after 2 secs
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
resolve("done")
}
}
它接受一个提供的闭包并返回一个新的 Promise。 闭包在当前 Promise 被解析时运行,并接收结果。
Promise {
try String(contentsOfFile: "file.txt")
}
.then { text in
print(text)
}
通过这种方式,我们可以通过 Promise 链传递结果。
Promise {
try String(contentsOfFile: "file.txt")
}
.then { text in
return text.count
}
.then { count in
print(count)
}
此外,闭包可以返回一个 Promise,它将被注入到 Promise 链中。
Promise {
return 200
}
.then { value in
Promise {
value / 10
}
}
.then { value in
print(value)
}
// Prints "20"
它接受一个闭包并返回一个新的 Promise。 闭包在 Promise 被拒绝时运行,并接收错误。
Promise {
try String(contentsOfFile: "nofile.txt") // Jumps to catch
}
.then { text in
print(text) // Doesn't run
}
.catch { error in
print(error.localizedDescription)
}
// Prints "The file `nofile.txt` couldn’t be opened because there is no such file."
无论 Promise 是被解析还是被拒绝,这都会始终运行,因此它是执行清理等操作的良好处理程序。
Promise {
try String(contentsOfFile: "file.txt")
}
.finally {
print("Finish reading") // Always runs
}
.then { text in
print(text)
}
.catch { error in
print(error.localizedDescription)
}
.finally {
print("The end") // Always runs
}
这些用于兼容性,例如,当只需要返回一个已解析或已拒绝的 Promise 时。
Promise.resolve
创建一个具有给定值的已解析 Promise。
Promise {
return 200
}
// Same as above
Promise.resolve(200)
Promise.reject
创建一个具有给定错误的已拒绝 Promise。
Promise {
throw error
}
// Same as above
Promise<Void>.reject(error)
它返回一个 Promise,该 Promise 在提供的列表中所有列出的 Promise 都被解析时解析,并且其结果数组将成为其结果。 如果任何 Promise 被拒绝,则 Promise.all
返回的 Promise 会立即被拒绝,并出现该错误。
Promise.all(
Promise {
return "Hello"
},
Promise { resolve, reject in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
resolve("World")
}
}
)
.then { results in
print(results)
}
// Prints ["Hello", "World"]
您可以设置 settled=true
参数以创建一个 Promise,该 Promise 在所有列出的 Promise 都被解决时解析,无论其结果如何。
Promise.all(settled: true,
Promise<Any> { resolve, reject in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
reject(error)
}
},
Promise {
return 200
}
)
.then { results in
print(results)
}
// Prints [error, 200]
如果没有 Promise 或 Promise 数组为空,则 Promise 使用空数组解析。
它创建一个 Promise,该 Promise 仅等待给定列表中第一个已解决的 Promise,并获取其结果或错误。
Promise.race(
Promise { resolve, reject in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { // Wait 2 secs
reject("Error")
}
},
Promise { resolve, reject in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // Wait 1 sec
resolve(200)
}
}
)
.then {
print($0)
}
// Prints "200"
如果没有 Promise 或 Promise 数组为空,则 Promise 会被 PromiseError.empty
错误拒绝。
它与 Promise.race
类似,但仅等待第一个已完成的 Promise,并获取其结果。
Promise.any(
Promise { resolve, reject in
reject("Error")
},
Promise { resolve, reject in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // Waits 1 sec
resolve(200)
}
}
)
.then {
print($0)
}
// Prints "200"
如果没有 Promise 或 Promise 数组为空,则 Promise 会被 PromiseError.empty
错误拒绝。
如果所有给定的 Promise 都被拒绝,则返回的 Promise 将被 PromiseError.aggregate
拒绝 - 这是一种特殊的错误,用于存储所有 Promise 错误。
Promise.any(
Promise { resolve, reject in
reject("Error")
},
Promise { resolve, reject in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // Waits 1 sec
reject("Fail")
}
}
)
.catch { error in
if case let PromiseError.aggregate(errors) = error {
print(errors, "-", error.localizedDescription)
}
}
// Prints: '["Error", "Fail"]' - All Promises rejected.
timeout
参数允许等待 Promise 一段时间间隔,如果 Promise 没有在给定时间内解析,则使用 PromiseError.timedOut
错误拒绝它。
Promise(timeout: 10) { // Wait 10 secs for data
try loadData()
}
.then(timeout: 1) { data in // Wait 1 sec for parsed data
try parse(data)
}
.catch(timeout: 1) { error in // Wait 1 sec to handle errors
if case PromiseError.timedOut = error {
print(error.localizedDescription)
}
else {
handleError(error)
}
}
retry
参数提供了在 Promise 被拒绝时重新尝试任务的能力。 默认情况下,只有一次尝试解析 Promise,但您可以使用此参数增加尝试次数。
Promise(retry: 3) { // Makes 3 attempts to load data after the rejection
try loadData()
}
.then { data in
parse(data)
...
}
.catch { error in
print(error.localizedDescription) // Calls if the `loadData` fails 4 times (1 + 3 retries)
}
这是一种特殊的表示法,可以更舒适地使用 Promise,并且易于理解和使用。
async
是 Promise
的别名,因此您也可以使用它来创建 Promise。
// Returns a promise with `String` type
func readFile(_ file: String) -> async<String> {
return async {
try String(contentsOfFile: file)
}
}
await()
是一个函数,同步地等待 Promise 的结果,否则会抛出错误。
let text = try readFile("file.txt").await()
// OR
let text = try `await` { readFile("file.txt") }
为了避免阻塞当前队列(例如主 UI 队列),我们可以将 await()
传递到另一个 Promise(async 块)中,并像往常一样使用 catch
来处理错误。
async {
let text = try readFile("file.txt").await()
print(text)
}
.catch { error in
print(error.localizedDescription)
}
suspend()
临时暂停一个 Promise。 暂停不会影响已经开始执行的当前 Promise,它会停止链中下一个 Promise 的执行。 Promise 可以在以后使用 resume()
继续执行。
let promise = Promise {
String(contentsOfFile: file)
}
promise.suspend()
...
// Later
promise.resume()
取消 Promise 的执行并使用 PromiseError.cancelled
错误拒绝它。 取消不会影响已经开始执行的 Promise,它会拒绝 Promise 并停止链中下一个 Promise 的执行。
let promise = Promise {
return "Text" // Never runs
}
.then { text in
print(text) // Never runs
}
.catch { error in
if case PromiseError.cancelled = error {
print(error.localizedDescription)
}
}
promise.cancel()
// Prints: The Promise cancelled.
取消 Promise 不会主动停止长时间运行的处理代码的执行。 您可以使用 isCancelled
方法检查 Promise 是否已取消,如果该方法返回 true
,则停止您的代码。
let promise = Promise { 10_000 }
promise.then { count in
for i in 0..<count {
guard !promise.isCancelled else {
return
}
...
}
}
您也可以在某些条件下中断 Promise 链,以便在 Promise 链的任何闭包中调用 cancel
,例如:
let promise = Promise {
return getStatusCode()
}
promise.then { statusCode in
guard statusCode == 200 else {
promise.cancel() // Breaks the promise chain
return
}
...
}
.then {
... // Never runs in case of cancel
}
Asyncable
协议表示可以暂停、恢复和取消的异步任务类型。
public protocol Asyncable {
func suspend() // Temporarily suspends a task.
func resume() // Resumes the task, if it is suspended.
func cancel() // Cancels the task.
}
Promise 可以在包装异步任务时管理它。 例如,它对于网络请求很有用。
// The wrapped asynchronous task must be conformed to `Asyncable` protocol.
extension URLSessionDataTask: Asyncable {
}
let promise = Promise<Data> { resolve, reject, task in // `task` is in-out parameter
task = URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil else {
reject(error!)
return
}
resolve(data)
}
task.resume()
}
// The promise and the data task will be suspended after 2 secs and won't produce any network activity.
// but they can be resumed later.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
promise.suspend()
}
您也可以创建自定义的异步任务,该任务可以由 Promise 管理。
class TimeOutTask : Asyncable {
let timeOut: TimeInterval
var work: DispatchWorkItem?
let fire: () -> Void
init(timeOut: TimeInterval, _ fire: @escaping () -> Void) {
self.timeOut = timeOut
self.fire = fire
}
// MARK: Asyncable
func suspend() {
cancel()
}
func resume() {
work = DispatchWorkItem(block: self.fire)
DispatchQueue.global().asyncAfter(deadline: .now() + timeOut, execute: work!)
}
func cancel() {
work?.cancel()
work = nil
}
}
// Promise
let promise = Promise<String> { resolve, reject, task in // `task` is in-out parameter
task = TimeOutTask(timeOut: 3) {
resolve("timed out") // Won't be called
}
task.resume()
}
.then { text in
print(text) // Won't be called
}
// Both the promise and the timed out task will be canceled after 1 sec
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
promise.cancel()
}
您可以使用异步 fetch
实用程序函数向服务器发送网络请求并在需要时加载新信息。 它立即启动请求并返回一个 Promise,调用代码应该使用该 Promise 来获取结果。 fetch
返回的 Promise 在服务器响应后立即使用 HTTPResponse
对象解析,您可以使用它来访问响应属性。
ok: Bool
- 如果 HTTP 状态代码为 200-299,则为 true
。statusCodeDescription: String
- 带有描述的 HTTP 状态代码。response: HTTPURLResponse
- 响应元数据对象,例如 HTTP 标头和状态代码。data: Data?
- 服务器返回的数据。text: String?
- 来自 data
的文本。json: Any?
- 来自 data
的 json。例如
fetch("https://api.github.com/users/technoweenie") // Get github user's info
.then { response in
guard response.ok else { // Check for the HTTP status code
throw response.statusCodeDescription
}
guard let json = response.json as? [String: Any] else { // Get json from the returned data
throw "No JSON"
}
if let name = json["name"] as? String { // Get name of the user
print(name)
}
}
.catch {error in
print(error.localizedDescription)
}
// Prints: risk danger olson
默认情况下,fetch
使用默认标头和空正文数据执行 GET
请求,但您可以使用可选参数更改它。
method: HTTPMethod
- HTTP 方法,例如 .GET
、.POST
等。headers: [String : String]?
- 一个字典,包含请求的所有 HTTP 标头字段。body: Data?
- 请求正文。retry: Int
- 在拒绝后尝试解析 Promise 的最大重试次数。async {
let response = try fetch(url,
method: .POST,
headers: ["Accept-Encoding" : "br, gzip, deflate"],
body: data
).await()
...
}
.catch {error in
print(error.localizedDescription)
}
默认情况下,fetch
使用 URLSession.default
发出请求,但您也可以在会话实例上调用它,并调整会话行为的各个方面,包括缓存策略、超时间隔等。
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 120
config.httpAdditionalHeaders = ["Accept-Encoding" : "br, gzip, deflate"]
let session = URLSession(configuration: config)
async {
let response = try session.fetch(url).await()
...
}
.catch {error in
print(error.localizedDescription)
}
download
的工作方式类似于 fetch
,但会将数据保存到文件并告知下载进度,例如:
async {
let response = try download("http://speedtest.tele2.net/1MB.zip") { task, written, total in
let percent = Double(written) / Double(total)
print(percent)
}.await()
guard response.ok else {
throw response.statusCodeDescription
}
guard let location = response.location else {
throw "No location"
}
print(location)
}
.catch { error in
print(error.localizedDescription)
}
// Prints
0.038433074951171875
0.10195541381835938
...
0.9263648986816406
1.0
file:///var/folders/nt/mrsc3jhd13j8zhrhxy4x23y40000gp/T/pq_CFNetworkDownload_t94Pig.tmp
upload
的工作方式类似于 fetch
,但可以将数据或文件上传到服务器并告知上传进度,例如:
async {
let response = try upload(url, data: data) { task, sent, total in
let percent = Double(sent) / Double(total)
print(percent)
}.await()
guard response.ok else {
throw response.statusCodeDescription
}
print("Uploaded")
}
.catch { error in
print(error.localizedDescription)
}
// Prints
0.03125
0.0625
...
0.96875
1.0
Uploaded
有两种代码变体来 fetch
前 30 个 GitHub 用户的头像。
使用 then
struct User : Codable {
let login: String
let avatar_url: String
}
fetch("https://api.github.com/users") // Load json with users
.then { response -> [User] in
guard response.ok else {
throw response.statusCodeDescription
}
guard let data = response.data else {
throw "No data"
}
return try JSONDecoder().decode([User].self, from: data) // Parse json
}
.then { users -> Promise<Array<HTTPResponse>> in
return Promise.all(
users
.map { $0.avatar_url }
.map { fetch($0) }
)
}
.then { responses in
responses
.compactMap { $0.data }
.compactMap { UIImage(data: $0)} // Create array of images
}
.then(.main) { images in
print(images.count) // Print a count of images on the main queue
}
.catch { error in
print(error.localizedDescription)
}
使用 async/await
async {
let response = try `await` { fetch("https://api.github.com/users") }
guard response.ok else {
throw response.statusCodeDescription
}
guard let data = response.data else {
throw "No data"
}
let users = try JSONDecoder().decode([User].self, from: data)
let images = try `await` {
Promise.all(
users.map { fetch($0.avatar_url) }
)
}
.compactMap { $0.data }
.compactMap { UIImage(data: $0) }
// Switch to the main queue
async(.main) {
print(images.count)
}
}
.catch { error in
print(error.localizedDescription)
}
有关更多示例,请参见 PromiseQTests.swift。
Xcode > File > Swift Packages > Add Package Dependency...
https://github.com/ikhvorost/PromiseQ.git
import PromiseQ
将 PromiseQ
程序包依赖项添加到您的 Package.swift
文件
let package = Package(
...
dependencies: [
.package(url: "https://github.com/ikhvorost/PromiseQ.git", from: "1.0.0")
],
targets: [
.target(name: "YourPackage",
dependencies: [
.product(name: "PromiseQ", package: "PromiseQ")
]
),
...
...
)
PromiseQ 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。