Swift 中轻量级、功能完善的 Promise、Async & Await 库
Hydra 是一个功能完善的轻量级库,它允许你在 Swift 3.x/4.x 中编写更好的异步代码。它部分基于 JavaScript A+ 规范,并且还实现了现代结构,例如 await
(如 ES8 (ECMAScript 2017) 中的 Async/Await 规范 或 C# 中所见),它允许你以同步的方式编写异步代码。 Hydra 支持所有最吸引人的操作符,例如 always、validate、timeout、retry、all、any、pass、recover、map、zip、defer 和 retry。
开始使用 Hydra 编写更好的异步代码!
有关 Hydra 如何工作的更详细的介绍可以在 ARCHITECTURE 文件中或 Medium 上找到。
你好,各位开发者!
你知道,维护和开发工具需要消耗资源和时间。 虽然我喜欢制作它们,你的支持对于让我继续开发至关重要。
如果你正在使用 SwiftLocation 或我的任何其他作品,请考虑以下选项
zip
操作符来解析所有 PromiseValue
类型的 PromisePromise 是一种表示在未来的某个时间点存在或因错误而失败的值的方法。 你可以把它想象成 Swift 的 Optional
:它可能是一个值,也可能不是。一篇更详细的文章解释了 Hydra 的实现方式可以在这里找到。
每个 Promise 都是强类型的:这意味着你使用期望的值类型创建它,并且你将确保在 Promise 被解析(确切的术语是 fulfilled
)时收到它。
事实上,Promise 是一个代理对象;由于系统知道成功值的样子,因此组合异步操作是一项简单的任务; 使用 Hydra 你可以
自 0.9.7 起,Hydra 实现了可取消的 Promise。 为了支持这个新特性,我们稍微修改了 Promise
的 Body
签名; 为了使你的源代码兼容,你只需要添加第三个参数以及 resolve
、reject
:operation
。 operation
封装了支持 Invalidation Token
的逻辑。 它只是一个类型为 PromiseStatus
的对象,你可以查询该对象以查看 Promise 是否从外部标记为已取消。 如果你对在 Promise 声明中使用它不感兴趣,只需将其标记为 _
。
总而言之,你的代码
return Promise<Int>(in: .main, token: token, { resolve, reject in ...
需要变为
return Promise<Int>(in: .main, token: token, { resolve, reject, operation in // or resolve, reject, _
创建一个 Promise 非常简单; 你需要指定 context
(一个 GCD 队列),你的异步操作将在其中执行,并将你自己的异步代码添加为 Promise 的 body
。
这是一个简单的异步图像下载器
func getImage(url: String) -> Promise<UIImage> {
return Promise<UIImage>(in: .background, { resolve, 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 {
resolve((data, response))
} else {
reject("Image cannot be decoded")
}
}).resume()
})
}
你只需要记住几件事
UIImage
,所以我们的 Promise 是 Promise<UIImage>
(如果 Promise 失败,返回的错误必须符合 Swift 的 Error
协议)body
中定义)必须向 Promise 发出有关其完成的警报; 如果你有 fulfill 值,你将调用 resolve(yourValue)
; 如果发生错误,你可以调用 reject(occurredError)
或使用 Swift 的 throw occurredError
抛出它。context
定义了将要在其中执行异步代码的 Grand Central Dispatch 的队列; 你可以使用定义的队列之一(.background
、.userInitiated
等。 你可以在这里找到关于此主题的不错的教程)使用 Promise 甚至更容易。
你可以使用 then
函数获取 Promise 的结果; 当你的 Promise 以预期值 fullfill 时,它将被自动调用。 所以
getImage(url).then(.main, { image in
myImageView.image = image
})
如你所见,即使 then
也可以指定一个上下文(默认情况下 - 如果未指定 - 是主线程):这表示将在其中执行 then 的代码块的 GCD 队列(在我们的例子中,我们需要更新一个 UI 控件,因此我们需要在 .main
线程中执行它)。
但是,如果你的 Promise 因网络错误或图像无法解码而失败怎么办? catch
函数允许你处理 Promise 的错误(使用多个 Promise,你也可以拥有一个单独的错误入口点并降低复杂性)。
getImage(url).then(.main, { image in
myImageView.image = image
}).catch(.main, { error in
print("Something bad occurred: \(error)")
})
链接 Promise 是掌握 Hydra 的下一步。 假设你定义了一些 Promise
func loginUser(_ name:String, _ pwd: String)->Promise<User>
func getFollowers(user: User)->Promise<[Follower]>
func unfollow(followers: [Follower])->Promise<Int>
每个 Promise 都需要使用前一个 Promise 的 fulfilled 值; 另外,其中一个错误应该中断整个链。
使用 Hydra 这样做非常简单
loginUser(username,pass).then(getFollowers).then(unfollow).then { count in
print("Unfollowed \(count) users")
}.catch { err in
// Something bad occurred during these calls
}
很简单,对吧?(请注意:在此示例中未指定上下文,因此使用默认的 .main
)。
可取消的 Promise 是一项非常敏感的任务;默认情况下,Promise 是不可取消的。 Hydra 允许您通过实现 InvalidationToken
从外部取消 promise。 InvalidationToken
是一个具体的开放类,它符合 InvalidatableProtocol
协议。 它必须至少实现一个名为 isCancelled
的 Bool
属性。
当 isCancelled
设置为 true
时,表示 promise 外部的某人想要取消该任务。
您有责任从 Promise
的主体内部通过询问 operation.isCancelled
来检查此变量的状态。 如果 true
,您可以尽最大努力取消操作; 在操作结束时,只需调用 cancel()
并停止工作流程。
您的 promise 还必须使用此 token 实例进行初始化。
这是一个带有 UITableViewCell
的具体示例:在使用表格单元格时,通常需要忽略 promise 的结果。 为此,每个单元格都可以保留一个 InvalidationToken
。 InvalidationToken
是一个可以失效的执行上下文。 如果上下文失效,则传递给它的块将被丢弃并且不会执行。
要将此与表格单元格一起使用,队列应在 prepareForReuse()
上失效并重置。
class SomeTableViewCell: UITableViewCell {
var token = InvalidationToken()
func setImage(atURL url: URL) {
downloadImage(url).then(in: .main, { image in
self.imageView.image = image
})
}
override func prepareForReuse() {
super.prepareForReuse()
token.invalidate() // stop current task and ignore result
token = InvalidationToken() // new token
}
func downloadImage(url: URL) -> Promise<UIImage> {
return Promise<Something>(in: .background, token: token, { (resolve, reject, operation) in
// ... your async operation
// somewhere in your Promise's body, for example in download progression
// you should check for the status of the operation.
if operation.isCancelled {
// operation should be cancelled
// do your best to cancel the promise's task
operation.cancel() // request to mark the Promise as cancelled
return // stop the workflow! it's important
}
// ... your async operation
})
}
}
您是否梦想过像编写同步代码一样编写异步代码? Hydra 受到 ES8 (ECMAScript 2017) 中的 Async/Await 规范 的启发,它提供了一种以顺序方式编写异步代码的强大方法。
使用 async
和 await
非常简单。
注意:从 Hydra 2.0.6 开始,await 函数在 Hydra.await() 函数下可用,以便抑制 Xcode 12.5+ 警告(await 很快将成为 Swift 标准函数!)
例如,上面的代码可以直接重写为
// With `async` we have just defined a Promise which will be executed in a given
// context (if omitted `background` thread is used) and return an Int value.
let asyncFunc = async({ _ -> Int in // you must specify the return of the Promise, here an Int
// With `await` the async code is resolved in a sync manner
let loggedUser = try Hydra.await(loginUser(username,pass))
// one promise...
let followersList = try Hydra.await(getFollowers(loggedUser))
// after another...
let countUnfollowed = try Hydra.await(unfollow(followersList))
// ... linearly
// Then our async promise will be resolved with the end value
return countUnfollowed
}).then({ value in // ... and, like a promise, the value is returned
print("Unfollowed \(value) users")
})
就像魔法一样! 您的代码将在 .background
线程中运行,并且您只有在每次调用完成时才能获得结果。 同步风格的异步代码!
重要提示:await
是一个使用信号量实现的阻塞/同步函数。 因此,它不应在主线程中调用; 这就是我们使用 async
对其进行封装的原因。 在主线程中执行它也会阻塞 UI。
async
函数可以在两种不同的选项中使用
正如我们所说,我们也可以将 async
与您自己的块一起使用(不使用 promise); async
接受上下文(一个 GCD 队列)和可选的启动延迟间隔。 下面是一个 async 函数的示例,它将在后台无延迟地执行
async({
print("And now some intensive task...")
let result = try! Hydra.await(.background, { resolve,reject, _ in
delay(10, context: .background, closure: { // jut a trick for our example
resolve(5)
})
})
print("The result is \(result)")
})
还有一个 await 运算符
..
后跟一个 Promise 实例:此运算符必须以 try
为前缀,并且应使用 do/catch
语句来处理 Promise 的拒绝。..!
后跟一个 Promise 实例:此运算符不会抛出异常; 在 promise 被拒绝的情况下,结果将为 nil。示例
async({
// AWAIT OPERATOR WITH DO/CATCH: `..`
do {
let result_1 = try ..asyncOperation1()
let result_2 = try ..asyncOperation2(result_1) // result_1 is always valid
} catch {
// something goes bad with one of these async operations
}
})
// AWAIT OPERATOR WITH NIL-RESULT: `..!`
async({
let result_1 = ..!asyncOperation1() // may return nil if promise fail. does not throw!
let result_2 = ..!asyncOperation2(result_1) // you must handle nil case manually
})
当您使用这些方法并且正在进行异步操作时,请注意不要在主线程中执行任何操作,否则您有陷入死锁的风险。
最后一个示例展示了如何使用可取消的 async
func test_invalidationTokenWithAsyncOperator() {
// create an invalidation token
let invalidator: InvalidationToken = InvalidationToken()
async(token: invalidator, { status -> String in
Thread.sleep(forTimeInterval: 2.0)
if status.isCancelled {
print("Promise cancelled")
} else {
print("Promise resolved")
}
return "" // read result
}).then { _ in
// read result
}
// Anytime you can send a cancel message to invalidate the promise
invalidator.invalidate()
}
Await 也可以与 zip 结合使用,以解析列表中的所有 promise
let (resultA,resultB) = Hydra.await(zip(promiseA,promiseB))
print(resultA)
print(resultB)
由于 promise 形式化了成功和失败块的外观,因此可以在它们之上构建行为。 Hydra 支持
always
:允许您指定一个块,该块将始终针对 Promise 的 fulfill
和 reject
执行validate
:允许您指定一个 predica 块; 如果谓词返回 false
,则 Promise 失败。timeout
:向 Promise 添加一个超时计时器; 如果它在给定的时间间隔后未履行或拒绝,它将被标记为拒绝。all
:创建一个 Promise,该 Promise 在传递的 Promise 列表解析时解析(promise 并行解析)。 Promise 也会因任何原因在 promise 拒绝时立即拒绝。any
:创建一个 Promise,该 Promise 在列表中传递的一个 promise 解析时立即解析。 它也会因任何原因在 promise 拒绝时立即拒绝。pass
:在链的中间执行一项操作,该操作不影响解析的值,但可能会拒绝链。recover
:允许通过在 Promise 失败时返回另一个 Promise 来恢复 Promise。map
:将项目转换为 Promise 并解析它们(并行或串行)zip
:创建两个 promise 的 Promise 元组defer
:按给定的时间间隔延迟 Promise 的执行。cancel
:当使用 operation.cancel()
将 promise 标记为 cancelled
时,将调用 cancel如果您想在 promise 完成时执行代码,而不管它是否成功或失败,always
函数非常有用。
showLoadingHUD("Logging in...")
loginUser(username,pass).then { user in
print("Welcome \(user.username)")
}.catch { err in
print("Cannot login \(err)")
}.always {
hideLoadingHUD()
}
validate
是一个采用谓词的函数,如果该谓词失败,则拒绝 promise 链。
getAllUsersResponse().validate { httpResponse in
guard let httpResponse.statusCode == 200 else {
return false
}
return true
}.then { usersList in
// do something
}.catch { error in
// request failed, or the status code was != 200
}
timeout
允许您将超时计时器附加到 Promise; 如果它在经过的时间间隔之前未解析,它将被 .timeoutError
拒绝。
loginUser(username,pass).timeout(.main, 10, .MyCustomTimeoutError).then { user in
// logged in
}.catch { err in
// an error has occurred, may be `MyCustomTimeoutError
}
all
是一种静态方法,它等待您给它的所有 promise 完成,一旦它们完成,它就会用所有已完成值的数组(按顺序)完成自身。
如果一个 Promise 失败,链也会失败,并显示相同的错误。
所有 Promise 的执行都是并行完成的。
let promises = usernameList.map { return getAvatar(username: $0) }
all(promises).then { usersAvatars in
// you will get an array of UIImage with the avatars of input
// usernames, all in the same order of the input.
// Download of the avatar is done in parallel in background!
}.catch { err in
// something bad has occurred
}
如果您想为 all
操作符添加 Promise 执行并发限制,以避免大量资源占用,可以使用 concurrency
选项。
let promises = usernameList.map { return getAvatar(username: $0) }
all(promises, concurrency: 4).then { usersAvatars in
// results of usersAvatars is same as `all` without concurrency.
}.catch { err in
// something bad has occurred
}
any
可以轻松处理竞争条件:只要输入列表中的一个 Promise 解决 (resolve),处理程序就会被调用,并且不会再次被调用。
let mirror_1 = "https://mirror1.mycompany.com/file"
let mirror_2 = "https://mirror2.mycompany.com/file"
any(getFile(mirror_1), getFile(mirror_2)).then { data in
// the first fulfilled promise also resolve the any Promise
// handler is called exactly one time!
}
pass
对于在 Promise 链的中间执行操作而不更改 Promise 的类型非常有用。您也可以拒绝整个链。您还可以从 tap 处理程序返回一个 Promise,并且链将等待该 Promise 解决(请参见下面的示例中的第二个 then
)。
loginUser(user,pass).pass { userObj in
print("Fullname is \(user.fullname)")
}.then { userObj in
updateLastActivity(userObj)
}.then { userObj in
print("Login succeded!")
}
recover
允许您通过返回另一个 Promise 来恢复失败的 Promise。
let promise = Promise<Int>(in: .background, { fulfill, reject in
reject(AnError)
}).recover({ error in
return Promise(in: .background, { (fulfill, reject) in
fulfill(value)
})
})
Map 用于将项目列表转换为 promise,并并行或串行地解决它们。
[urlString1,urlString2,urlString3].map {
return self.asyncFunc2(value: $0)
}.then(.main, { dataArray in
// get the list of all downloaded data from urls
}).catch({
// something bad has occurred
})
zip
允许您连接不同的 promise (2, 3 或 4) 并返回一个包含它们结果的元组。 Promise 是并行解决的。
zip(a: getUserProfile(user), b: getUserAvatar(user), c: getUserFriends(user))
.then { profile, avatar, friends in
// ... let's do something
}.catch {
// something bad as occurred. at least one of given promises failed
}
顾名思义,defer
会将 Promise 链的执行从当前时间延迟若干秒。
asyncFunc1().defer(.main, 5).then...
如果源链接的 promise 以拒绝 (rejection) 结束,则 retry
操作符允许您执行源链接的 promise。如果达到尝试次数,promise 仍然被拒绝,链接的 promise 也会被拒绝,并伴随相同的源错误。
Retry 还支持 delay
参数,该参数指定在新的尝试之前要等待的秒数 (2.0.4+)。
// try to execute myAsyncFunc(); if it fails the operator try two other times
// If there is not luck for you the promise itself fails with the last catched error.
myAsyncFunc(param).retry(3).then { value in
print("Value \(value) got at attempt #\(currentAttempt)")
}.catch { err in
print("Failed to get a value after \(currentAttempt) attempts with error: \(err)")
}
条件重试允许您控制如果重试以拒绝结束是否可重试。
// If myAsyncFunc() fails the operator execute the condition block to check retryable.
// If return false in condition block, promise state rejected with last catched error.
myAsyncFunc(param).retry(3) { (remainAttempts, error) -> Bool in
return error.isRetryable
}.then { value in
print("Value \(value) got at attempt #\(currentAttempt)")
}.catch { err in
print("Failed to get a value after \(currentAttempt) attempts with error: \(err)")
}
当通过调用 operation.cancel()
函数从 Promise 的主体将 promise 标记为 cancelled
时,将调用 cancel
。 有关更多信息,请参见 可取消的 Promises。
asyncFunc1().cancel(.main, {
// promise is cancelled, do something
}).then...
有时您可能需要链接(使用可用的操作符之一,例如 all
或 any
)返回不同类型值的 promise。 由于 Promise 的性质,您无法创建具有不同结果类型的 promise 数组。 但是,借助 void
属性,您可以将 promise 实例转换为通用的 void
结果类型。 因此,例如,您可以执行以下 Promises
并直接从 Promise 的 result
属性返回最终值。
let op_1: Promise<User> = asyncGetCurrentUserProfile()
let op_2: Promise<UIImage> = asyncGetCurrentUserAvatar()
let op_3: Promise<[User]> = asyncGetCUrrentUserFriends()
all(op_1.void,op_2.void,op_3.void).then { _ in
let userProfile = op_1.result
let avatar = op_2.result
let friends = op_3.result
}.catch { err in
// do something
}
您可以使用 CocoaPods、Carthage 和 Swift 包管理器安装 Hydra
pod 'HydraAsync', ~> '1.0.2'
pod 'HydraAsync'
use_frameworks!
pod 'HydraAsync'
github 'malcommac/Hydra'
在您的 Package.swift
中将 Hydra 添加为依赖项
import PackageDescription
let package = Package(name: "YourPackage",
dependencies: [
.Package(url: "https://github.com/malcommac/Hydra.git", majorVersion: 0),
]
)
考虑 ❤️ 支持此库的开发!
当前版本兼容:
SwiftLocation 目前由 Daniele Margutti 拥有和维护。
您可以在 Twitter 上关注我:@danielemargutti。
我的网站是:https://www.danielemargutti.com
本软件基于 MIT 许可证授权。
关注我: