与 Hydra 一起再次爱你异步代码
使用
★★ 请星标我们的 GitHub 仓库以帮助我们! ★★
由 Daniele Margutti (@danielemargutti) 创建
Swift 3 和 Swift 4 兼容性
- Swift 4.x:最新版本是 1.2.1(《pod 'HydraAsync'》)
- Swift 3.x:最新版本是 1.0.2 - 兼容版本是 1.0.2,在这里下载。如果您使用 CocoaPods,请确保修复发行版(《pod 'HydraAsync', '~> 1.0.2'》)
Hydra
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上发表的文章中查看。
您可能会喜欢的其他库
我还在开发其他几个您可能会喜欢的项目。下面看一下
库 | 描述 |
---|---|
SwiftDate | 管理Swift中日期/时区最佳方式 |
Hydra | 编写更好的异步代码:async/await & promises |
Flow | 表格管理的新声明式方法。忘掉dataSource & delegates。 |
SwiftRichString | Swift中优雅且无痛的NSAttributedString |
SwiftLocation | 高效的位置管理器 |
SwiftMsgPack | 快速/高效的msgPack编码/解码器 |
当前版本(Swift 3和4版本)
最新版本如下
- Swift 4.x:最新版本为1.2.0(
pod 'HydraAsync'
)在此下载。 - Swift 3.x:最新版本为1.0.1(《pod 'HydraAsync', '~> 1.0.1'》)在此下载。
每个版本的完整改动列表可在CHANGELOG文件中找到。
索引
- 什么是Promise
- 更新到 >=0.9.7
- 创建一个Promise
- 如何使用Promise
- 链式调用多个Promise
- 可取消的Promise
- Await & Async:以同步方式编写异步代码
- 等待
zip
运算符解决所有Promise - 所有功能
- 链式调用不同
Value
类型的Promise - 安装(CocoaPods、SwiftPM和Carthage)
- 要求
- 鸣谢
什么是Promise?
Promise(承诺)是一种表示未来某个时刻将存在值或失败时的错误的方式。你可以将其视为Swift的Optional
:它可能或不包含值。一篇更详细的关于Hydra如何实现的文章可以在这里找到。
每个Promise都有强类型:这意味着你将用你期望的值的类型创建它,并在Promise被解决(确切术语为fulfilled
)时确信收到它。
实际上,Promise是一个代理对象;由于系统知道成功值的外观,组合异步操作是一件简单的事情;使用Hydra你可以
- 通过一个单独的完成任务和单一个错误处理器创建一系列依赖于异步操作。
- 同时解决许多独立的异步操作并在最后获取所有值
- 重试或恢复失败的异步操作
- 像编写标准同步代码那样编写异步代码
- 通过将每个值的返回结果传递给下一个操作来解决依赖的异步操作,然后获取最终结果
- 避免回调、灾难性的金字塔,让你的代码更清晰!
升级到>=0.9.7
从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
创建Promise很简单;你需要指定异步操作将在其中执行的context
(一个GCD Queue),并将你自己的异步代码作为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()
})
}
你需要记住以下几点
- Promise是用一个类型创建的:这是当你完成时期望从对象中获取的类型。在我们的例子中,我们期望一个
UIImage
,所以我们的Promise是Promise<UIImage>
(如果Promise失败,返回的错误必须遵循Swift的Error
协议) - 你的异步代码(在Promise的
body
中定义)必须提醒Promise其完成;如果你有完成值,你会调用resolve(yourValue)
;如果发生错误,你可以调用reject(occurredError)
或使用Swift的throw occurredError
抛出它。 - Promise 的
context
定义了异步代码将在其中执行的 Grand Central Dispatch (GCD) 队列;您可以使用定义的队列之一(如.background
、.userInitiated
等)。在这里您可以找到关于这一主题的精彩教程。
如何使用 Promise
使用 Promise 非常简单。
您可以通过使用 then
函数来获取 Promise 的结果;当 Promise 成功满足预期的值时,它将被自动调用。所以
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
链式 Promises 是掌握 Hydra 的下一步。假设您已经定义了一些 Promise:
func loginUser(_ name:String, _ pwd: String)->Promise<User>
func getFollowers(user: User)->Promise<[Follower]>
func unfollow(followers: [Follower])->Promise<Int>
每个 Promise 都需要使用前一个 Promise 完成的值;此外,如果其中任何一个出现错误,应该中断整个链。
使用 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 是一项非常敏感的任务;默认情况下,Promise 不可取消。Hydra 允许您通过实现 InvalidationToken
来从外部取消 Promise。InvalidationToken
是一个具体的开源类,符合 InvalidatableProtocol
协议。它必须实现一个名为 isCancelled
的至少一个 Bool
属性。
当 isCancelled
设置为 true
时,这意味着有人在外部想要取消这个任务。
检查这个变量的状态是你的责任。你需要在 Promise
的内部通过询问 operation.isCancelled
来实现。如果它返回 true
,你可以尽力取消操作;在操作结束时,只需调用 cancel()
来停止工作流。
你的 promise 也必须使用此令牌实例进行初始化。
这是一个使用 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
})
}
}
等待 & Async:以同步方式执行异步代码
你是否曾梦想过能像写同步代码一样写异步代码?Hydra受到了一系列启发,其中最重要的是来自ES8 (ECMAScript 2017) 中 Async/Await 规范的,它提供了一个强大的方式来顺序地编写异步代码。
使用 async
和 await
非常简单。例如,上面的代码可以直接重写为
// 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 await(loginUser(username,pass))
// one promise...
let followersList = try await(getFollowers(loggedUser))
// after another...
let countUnfollowed = try 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
函数可以使用两种不同的选项
- 它可以从头创建并返回一个 promise(如你所见之上)
- 它被用来简单地执行一块代码(如你下面将看到的)
正如我们所说的,我们还可以使用 async
与你自己的代码块(不使用 promises);async
接受上下文(一个 GCD 队列)以及可选的启动延迟间隔。以下是一个示例 async 函数,它将在后台无延迟执行
async({
print("And now some intensive task...")
let result = try! await(.background, { resolve,reject, _ in
delay(10, context: .background, closure: { // jut a trick for our example
resolve(5)
})
})
print("The result is \(result)")
})
还有 await 操作符
- 带有抛出的 await:
..
后跟一个 Promise 实例:此操作符必须由try
前缀,并应该使用do/catch
语句处理 Promise 的拒绝。 - 无抛出的 await:
..!
后跟一个 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()
}
等待 zip
操作符解决所有 promise
Await 可与 zip 同时使用,以解决列表中所有 promise
let (resultA,resultB) = await(zip(promiseA,promiseB))
print(resultA)
print(resultB)
所有功能
由于 promises 规范化了成功和失败块的外观,可以在它们之上构建行为。Hydra 支持
always
:允许您指定一个块,该块将在 Promise 的fulfill
和reject
时始终执行validate
:允许您指定一个谓词块;如果谓词返回false
,则 Promise 失败。timeout
:向 Promise 添加超时计时器;如果在给定的时间间隔后没有解决或拒绝,则将其标记为拒绝。all
:创建一个 resolved 时列表中所有 passed Promises 都解决的 Promise(Promise 是并行解决的)。Promise 也会立即拒绝,只要任何 promise 因任何原因拒绝。any
:创建一个一旦 passed 列表中的某个 promise 解决就解决的 Promise。它也会立即拒绝,只要任何 promise 因任何原因拒绝。pass
:在链的中间执行操作,不会影响 resolved 的值,但可能会拒绝链。recover
:如果失败,允许回滚 Promise,通过返回另一个 Promise。map
:将项目转换为 Promise 并解决它们(并行或系列解决)zip
:创建一个两个 promises 的 Promise 元组defer
:将 Promise 的执行延迟给定的时间间隔。cancel
:当 promise 标记为cancelled
时使用operation.cancel()
调用 cancel
always
always
函数在您想要在 promise 展现时执行代码时非常有用,无论它成功或失败。
showLoadingHUD("Logging in...")
loginUser(username,pass).then { user in
print("Welcome \(user.username)")
}.catch { err in
print("Cannot login \(err)")
}.always {
hideLoadingHUD()
}
validate
validate
是一个函数,它接受一个谓词,并拒绝如果谓词失败。
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
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
all
是一个静态方法,它等待您给出的所有 Promises 都得到满足,一旦它们满足,它就使用所有满足的值的数组(按顺序)来满足自己。
如果其中一个 Promise 失败,链式处理将使用相同的错误失败。
所有 Promises 的执行都是并行的。
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
}
如果您要将 Promise 执行并发限制添加到 all
操作符中,以避免过度使用资源,请使用 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
any
简单处理竞态条件:一旦输入列表中的 Promise 之一解析,就会调用处理程序,并且不会被再次调用。
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
pass
对于在中间不改变 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
recover
允许您通过返回另一个来恢复失败的 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
“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
`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
正如其名所示,defer
会通过从当前时间起延迟一定 秒数来执行Promise链的执行。
asyncFunc1().defer(.main, 5).then...
retry
retry
操作符允许您在源链式promise结束时执行源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)")
}
cancel
cancel
会在promise被标记为cancelled
时被调用,可以通过在Promise体内部调用operation.cancel()
函数来实现。有关更多信息,请参阅
asyncFunc1().cancel(.main, {
// promise is cancelled, do something
}).then...
使用不同类型值的Promise链式调用
有时你可能需要链式调用(使用例如 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。
- Swift 3.x:最新兼容版本是1.0.2
pod 'HydraAsync', ~> '1.0.2'
- Swift 4.x:1.2.1或更高版本
pod 'HydraAsync'
CocoaPods
use_frameworks!
pod 'HydraAsync'
Carthage
github 'malcommac/Hydra'
Swift包管理器
在您的 Package.swift
中添加Hydra作为依赖项。
import PackageDescription
let package = Package(name: "YourPackage",
dependencies: [
.Package(url: "https://github.com/malcommac/Hydra.git", majorVersion: 0),
]
)
需求
当前版本与以下兼容
- Swift 4 (≥1.2.1)或 Swift 3.x(至1.0.2)
- iOS 9.0或更高版本
- tvOS 9.0或更高版本
- macOS 10.10或更高版本
- watchOS 2.0或更高版本
- 支持Linux环境
致谢 & 许可证
Hydra 由 Daniele Margutti 拥有和维护。
作为开源项目,任何帮助都是受欢迎的!
此库的代码受 MIT 许可证许可;您可以在商业产品中使用它而没有任何限制。
唯一的要求是在您的致谢/关于部分中添加以下文字的行
This software uses open source Hydra's library to manage async code.
Web: http://github.com/malcommac/Hydra
Created by Daniele Margutti and licensed under MIT License.