PromiseKit 咱们都在项目中见过,是典型的一看就会,一用就废的技能。写 PromiseKit 代码想必咱们都经历过如下图的过错分配的惊骇。
经过本文,让咱们深入浅出重新认识 PromiseKit。
概念考古
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事情——更合理和更强大。它由社区最早提出和完结,ES6 将其写进了语言规范,一致了用法,原生供给了 Promise
目标。
所谓 Promise
,简略说便是一个容器,里边保存着某个未来才会完毕的事情(通常是一个异步操作)的成果。从语法上说,Promise 是一个目标,从它能够获取异步操作的消息。Promise 供给一致的 API,各种异步操作都能够用相同的办法进行处理。
Promise
目标有以下两个特点:
- 目标的状况不受外界影响。
Promise
目标代表一个异步操作,有三种状况:pending
(进行中)、fulfilled
(已成功)和rejected
(已失利)。只要异步操作的成果,能够决议当时是哪一种状况,任何其他操作都无法改动这个状况。这也是Promise
这个姓名的由来,它的英语意思便是“许诺”,表明其他手法无法改动。 - 一旦状况改动,就不会再变,任何时分都能够得到这个成果。
Promise
目标的状况改动,只要两种或许:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种状况发生,状况就凝结了,不会再变了,会一直坚持这个成果,这时就称为 resolved(已定型)。假如改动现已发生了,你再对Promise
目标增加回调函数,也会当即得到这个成果。这与事情(Event)彻底不同,事情的特点是,假如你错过了它,再去监听,是得不到成果的。
有了 Promise
目标,就能够将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
目标供给一致的接口,使得控制异步操作愈加容易。
PromiseKit
Promise 是对异步操作的封装。状况固定。Promise 对异步回来的成果与过错进行了封装。
PromiseKit 是 Max Howell(brew 作者)小而美的著作。PromiseKit 为 Swift 带来了 Promise 的语义完结,对于 Swift 来说,更重要的意义是:
- 提升代码可读性。封装与链式拼接异步逻辑,避免写出回调地狱代码。一起也便于在异步流程中刺进或删去逻辑。
- 便捷地多线程调度办法。
简略来说 Promise 便是接纳上一级的成果,处理,异步回来成果。这是在 Swift 5.5+ async/await 之前引荐的异步逻辑封装办法。
与 Rx 的区分
最显著:
- Promise 回来一个成果。
- Observable 回来多个成果,是一个序列流。
初衷:
- Rx 致力于改动编程办法,把代码重构为可交互的管道式矩阵。
- Promise 只着眼于异步使命的办理。
完结上:
- Promise 的一切元素都运用相同的形式。Rx 则非常全面地供给了内部元素的相互操作。
- Rx 构建的事情链条不必定会主动停止,所以需求承担必定的垃圾回收。而 Promise 都会生成一个状况,停止时则开释自身。
运用场景
适用:
- 或许异步,回来一个成果,或失利,或成功。
不适用:
- 回来多个成果的序列流。
即要运用 PromiseKit,需求把要封装的逻辑笼统成一个异步事情,这个事情只要一个成果。
中心类型
首先理解一些关键词:
- Promise:一个异步行为的封装目标。
- Guarantee:与 Promise 相似,也是异步行为的封装目标,但不会发生过错。
- pending:待定,Promise 的初始状况。
- resolved:Promise 的完毕状况。完毕状况又可分为:
- fulfill:完结,Promise 的成功状况。
- reject:拒绝,Promise 的失利状况。
这些关键词都会在 PromiseKit 的 API 中高频呈现。
Result
PromiseKit 中表达成果的枚举,与 Swift 规范库中的 Result 有殊途同归、不同风格的表达办法。如其间的定义,成果只要成功和失利两个状况。
enum Result<T> {
case fulfilled(T)
case rejected(Error)
}
Resolver
能够理解为是生成 Result 的目标,用于结构 Promise 目标。供给了 fulfill
和 reject
办法标记成功和失利的成果。
func fulfill(_ value: T)
func reject(_ error: Error)
func resolve(_ result: Result<T>)
func resolve(_ obj: T?, _ error: Error?)
func resolve(_ obj: T, _ error: Error?)
func resolve(_ error: Error?, _ obj: T?)
Promise
或许会失利的异步封装目标。范型类型,范型是指成功值的类型。
class Promise<T>: Thenable, CatchMixin
Guarantee
不会抛出过错的异步封装目标。范型类型,范型是成果值的类型。
class Guarantee<T>: Thenable
两者的差异能够用图表明为:
Thenable
为 Promise 和 Guarantee 目标都遵从的协议,供给了拼接 Promise/Guarantee 并供给其他原语的能力。
/// 拼接 Promise/Guarantee,当其状况为成功时,履行拼接的 Promise/Guarantee。body 中回来的类型不必跟上一级使命的一致。
func then<U: Thenable>(_ body: @escaping(T) throws -> U) -> Promise<U.T>
/// 获取值,表明成功完毕。注意其 body 不必回来值,后续也不能拼接获取值的 Promise/Guarantee。
func done(_ body: @escaping(T) throws -> Void) -> Promise<Void>
/// 获取值。也是仅仅获取值,且 body 中不必回来,后续能够持续拼接获取值的 Promise/Guarantee,即不会对后续拼接流程有副作用。
func get(_ body: @escaping (T) throws -> Void) -> Promise<T>
/// 获取 Result 目标,且 body 中不必回来。相同也是不会对后续拼接流程发生副作用。
func tap(_ body: @escaping(Result<T>) -> Void) -> Promise<T>
/// 值转化,相同要求状况为成功时才履行。
func map<U>(_ transform: @escaping(T) throws -> U) -> Promise<U>
func map<U>(_ keyPath: KeyPath<T, U>) -> Promise<U>
func compactMap<U>(_ transform: @escaping(T) throws -> U?) -> Promise<U>
func compactMap<U>(_ keyPath: KeyPath<T, U?>) -> Promise<U>
以上的接口都包括这些参数,为了便于阅读进行了省略:
on: DispatchQueue? = conf.Q.map
flags: DispatchWorkItemFlags? = nil
class Guarantee<T>: Thenable
CatchMixin
Promise 遵从了 CatchMixin,也是跟 Guarantee 的重要差异,即 Promise 能够失利和处理失利。遵从了 CatchMixin 的 Promise 能够经过 catch
原语一致处理拼接的 Promise。一旦有过错就会落入 catch
原语中。
/// 一致处理过错。与 done 相似,body 中也是不会回来。catch 后边只能再拼接 `PMKFinalizer.finally(on:flags:_:)`,不能再拼接其他 Promise。
func `catch`(_ body: @escaping(Error) -> Void) -> PMKFinalizer
/// 过错处理和康复,body 中回来 Promise,使其能够后续拼接其他 Promise 履行。
func recover<U: Thenable>(_ body: @escaping(Error) throws -> U) -> Promise<T> where U.T == T
func recover(_ body: @escaping(Error) -> Guarantee<T>) -> Guarantee<T>
/// 获取值或过错,无论成功与否都会进入。body 中不必回来,不会对后续拼接流程发生副作用。用于在 catch 之前拼接;final 则在 catch 之后拼接。
func ensure(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Void) -> Promise<T>
以上的接口都包括这些参数,为了便于阅读进行了省略:
on: DispatchQueue? = conf.Q.map
flags: DispatchWorkItemFlags? = nil
policy: CatchPolicy = conf.catchPolicy
每个 promise 都是一个表明单个(individual)异步使命的目标。假如使命失利,它的 promise 将成为 rejected
。发生 rejected
promises 将跳过后边一切的 then
,而是将履行 catch
(严格上说是履行后续一切的 catch
处理)。
大局函数
/// 语法糖,用来包装一个 Promise/Guarantee,仅仅简略回来。
func firstly<U: Thenable>(execute body: () throws -> U) -> Promise<U.T>
func firstly<T>(execute body: () -> Guarantee<T>) -> Guarantee<T>
/// 把多个 promise 并联,并行履行,都完结后履行后边使命。相似于运用 DispatchGroup,内部运用 barrier 封装。
func when<U: Thenable>(fulfilled thenables: [U]) -> Promise<[U.T]>
/// 与 when 相反,当有最先完结的就会履行后边的使命。
func race<U: Thenable>(_ thenables: [U]) -> Promise<U.T>
运用
PromiseKit 运用的难点在于结构和拼接。运用图来表明:
写成代码:
这儿值得注意的是,由于运用原语衔接时是承接上一个 Promise 的成功成果的,并且给下一个衔接的 Promise 供给入参,这承上启下的类型必定要对应上,否则就会类型不匹配的编译过错。
结构
Promise
static Promise.value(_:)
:用值结构已成功的 Promise 目标。
- 用于直接回来包括成功值的 Promise 目标。
static func value(_ value: T) -> Promise<T>
guard foo else {
return .value(bar)
}
Promise.init(error:)
:用过错目标结构已失利的 Promise 目标。
- 用于直接回来包括过错的 Promise 目标。
init(error: Error)
Promise.init(resolver:)
:运用闭包结构 Promise 目标。
- 用于把异步办法封装成 Promise 目标。
- 对于要求回来 Promise 目标的场景,能够充分利用 Swift 的类型推断机制来削减类型声明。
init(resolver body: (Resolver<T>) throws -> Void)
let p3 = Promise<Void> { seal in
check { seal.fulfill_() }
}
let p4 = Promise<String> { seal in
fetch { result, error in
seal.resolve(result, error)
}
}
class Promise.pending()
:运用 pending 元组来构建 Promise 目标。
- 用于在多个异步回调中拼接 Promise 目标。
- 比照
Promise.init(resolver:)
,能够削减闭包的嵌套。
class func pending() -> (promise: Promise<T>, resolver: Resolver<T>)
func fileExistsAsync(forKey key: String) -> Promise<Bool> {
let path = path(forKey: key)
let pending = Promise<Bool>.pending()
queue.addOperation {
let result = FileManager.default.fileExists(atPath: path)
def.resolver.resolve(result, nil)
}
return pending.promise
}
多次重试示例:
func attempt<T>(maximumRetryCount: Int = 3, delayBeforeRetry: DispatchTimeInterval = .seconds(2), _ body: @escaping () -> Promise<T>) -> Promise<T> {
var attempts = 0
func attempt() -> Promise<T> {
attempts += 1
return body().recover { error -> Promise<T> in
guard attempts < maximumRetryCount else { throw error }
return after(delayBeforeRetry).then(on: nil, attempt)
}
}
return attempt()
}
attempt(maximumRetryCount: 3) {
fetch(url: url)
}.then {
//…
}.catch { _ in
// we still failed
}
Guarantee
办法根本与 Promise 相似,差异是不会发生过错,因而语法愈加简略。
/// 运用值结构 Guarantee 目标。
static func value(_ value: T) -> Guarantee<T>
/// 运用闭包结构 Guarantee 目标。
init(resolver body: (@escaping(T) -> Void) -> Void)
/// 运用 pending 元组来构建 Guarantee 目标。
class func pending() -> (guarantee: Guarantee<T>, resolve: (T) -> Void)
示例
下面以恳求相册权限和保存图片到相册两个异步事情为例,来演示 PromiseKit 的运用。
恳求相册权限
先来看看不运用 PromiseKit 的版别:
func requestPhotosAuthorityIfNeed(success: @escaping () -> Void, failure: @escaping (PhotosAuthorityError) -> Void) {
// 已授权的直接回来
guard !check(status: PHPhotoLibrary.authorizationStatus()) else {
success()
return
}
// 其他的进行权限恳求
PHPhotoLibrary.requestAuthorization { status in
if check(status: status) {
success()
} else {
failure(error(status: status))
}
}
}
办法还用到了一些过错的定义和工具办法:
enum CommonError: Error {
case unknown
}
enum PhotosAuthorityError: Error {
case restricted, denied, unknown
}
func error(status: PHAuthorizationStatus) -> PhotosAuthorityError {
switch status {
case .restricted: return .restricted
case .denied: return .denied
default: return .unknown
}
}
func check(status: PHAuthorizationStatus) -> Bool {
switch status {
case .authorized: return true
default: return false
}
}
运用 Promise.init(resolver:)
办法结构:
func requestPhotosAuthorityIfNeed() -> Promise<Void> {
guard !check(status: PHPhotoLibrary.authorizationStatus()) else {
return .value(())
}
return Promise { seal in
PHPhotoLibrary.requestAuthorization { status in
if check(status: status) {
seal.fulfill_()
} else {
seal.reject(error(status: status))
}
}
}
}
运用 class Promise.pending()
办法结构,这种办法能够削减闭包嵌套的层数:
func requestPhotosAuthorityIfNeed_() -> Promise<Void> {
guard !check(status: PHPhotoLibrary.authorizationStatus()) else {
return .value(())
}
let pending = Promise<Void>.pending()
PHPhotoLibrary.requestAuthorization { status in
if check(status: status) {
pending.resolver.fulfill_()
} else {
pending.resolver.reject(error(status: status))
}
}
return pending.promise
}
保存图片到相册
先来看看不运用 PromiseKit 的版别:
func saveImageToAlbum(url: URL, success: @escaping () -> Void, failure: @escaping (Error?) -> Void) {
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
} completionHandler: { finished, error in
// 回调这儿不为主行列,需切回主行列回调。
DispatchQueue.main.async {
if finished {
success()
} else {
failure(error)
}
}
}
}
运用 Promise.init(resolver:)
办法结构,Promise 运用原语拼接时回默许切回主行列,所以这儿不需求 DispatchQueue.main.async
:
func saveImageToAlbum(url: URL) -> Promise<Void> {
Promise { seal in
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
} completionHandler: { finished, error in
if finished {
seal.fulfill_()
} else {
seal.reject(error ?? CommonError.unknown)
}
}
}
}
运用 class Promise.pending()
办法结构:
func saveImageToAlbum_(url: URL) -> Promise<Void> {
let pending = Promise<Void>.pending()
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
} completionHandler: { finished, error in
if finished {
pending.resolver.fulfill_()
} else {
pending.resolver.reject(error ?? CommonError.unknown)
}
}
return pending.promise
}
组合运用
有了结构 Promise 的办法,那么要完结一个先恳求相册权限,再保存图片的逻辑就很简略,一起还一致了成功和失利的处理逻辑:
firstly {
requestPhotosAuthorityIfNeed()
}.then {
saveImageToAlbum(url: url)
}.done {
print("Save successfully.")
}.catch { error in
print("Save failed, error: \(error)")
}
假如现存代码现已有 requestPhotosAuthorityIfNeed(success:failure:)
和 saveImageToAlbum(url:success:failure:)
这两个中心逻辑,咱们也能够直接 inline 拼接 Promise 运用。这样,既能用到 PromiseKit 的优势,也避免了过度的封装。
firstly {
Promise<Void> { seal in
requestPhotosAuthorityIfNeed {
seal.fulfill_()
} failure: {
seal.reject($0)
}
}
}.then {
Promise<Void> { seal in
saveImageToAlbum(url: url) {
seal.fulfill_()
} failure: {
seal.reject($0 ?? CommonError.unknown)
}
}
}.done {
print("Save successfully.")
}.catch { error in
print("Save failed, error: \(error)")
}
最佳实践
聊完 PromiseKit 的根本运用,下面给出一些经验之谈,或许能让新同学避开不少坑。
- 坚持
firstly
最初,尽管该函数尽管没有本质作用,但能让代码坚持整齐和高雅。 - 必须明晰原语闭包中输入参数列表和输出类型,回来 Promise 目标的时分要对应上,否则呈现编译过错。
-
done
、get
、tap
原语中的闭包无需回来值;但then
、map
、recover
原语都需在 body 闭包中回来 Promise/Guarantee 目标。当闭包中只要一行时能够直接省略 return 写 Promise 目标。但有多行,则还需补上 body 闭包的入参、回来类型和 return 的声明。典型的比如如,原本 then 闭包中只要一个 Promise 的结构函数,后来在 Promise 结构函数之前加了一行代码,如 print,就编译不过了,这就需求弥补回来类型和 return 的声明。 -
then
拼接的 body 中能够回来与上级 Promise/Guarantee 目标不同的范型类型,能够用于组合多个成果,这与map
会有些奇妙的不同,如:
login().then { username in
fetch(avatar: username).map { ($0, username) }
}.then { image, username in
//…
}
- 尽或许避免多级闭包的嵌套,而是运用一个 Promise/Guarantee 目标表达一级异步闭包,即一个 Promise 只做一件事。运用链式拼接多级异步逻辑。这样更有利于后续刺进或删去其间的逻辑。
- 优先选择 Promise 进行封装异步逻辑。Guarantee 不能回来过错,为了后续扩展,答应失利的 Promise 能供给更高的灵活性。
- 运用
catch
一致处理过错,而不是在单个 Promise 中处理。 - 要一起处理过错和成功值,运用
ensure
。finally
尽管语义相似,但不能接纳上一步的成果。 - 运用
when
和race
处理多个 Promise/Guarantee 目标需一起履行的状况。 - 编写业务逻辑办法不必急于运用 Promise/Guarantee 目标封装,后续需求拼接的时分才进行封装,如:
firstly {
Promise<Void> { seal in
checkStorageSpaceByExportFileSize { seal.fulfill(()) }
}
}.then {
Promise<Void> { seal in
AuthorityManager.requestPhotos(with: "permission_request_album_export".L, success: { seal.fulfill(()) })
}
}.done {
self.presentExport(draft: draft, extra: self.makeExtraInfo())
}.catch { error in
}
- 运用
on
参数切换使命履行的线程行列,而不必另外封装 Promise/Guarantee 目标。一切的 Promise/Guarantee 都会在后台履行,但传递链自身(then()、catch()、map() 等)默许会在主线程履行,能够增加 on 参数履行的行列。这能够让封装的操作在指定的行列中履行。 - 运用
get
/tap
/then
刺进不影响流程的逻辑,而不是在原有的闭包中刺进。
[weak self] in PromiseKit
这个话题现已在 [swift – Should I use weak self] in PromiseKit blocks? – Stack Overflow 描绘得很清晰,这儿再烦琐翻译一遍。
评论在 PromiseKit 中是否需求运用 [weak self],其实是评论在逃逸闭包中是否需求运用 [weak self]。非逃逸的就彻底没有必要运用 [weak self] 哈。
先给出定论:PromiseKit 中,只要在需求在 self 开释时就停止闭包中的逻辑时,才运用 [weak self]。但无论何时尽或许避免运用 [unowned self]。
在闭包中运用 [weak self] 主要是为了:
- 避免引证循环(retain cycle);
- 避免延伸 self 指向目标的生命周期。
Promise 创立时的确会捕获外部目标,运用 self 也会持有 self 指向的目标。但如文章最初说到的 Promise 必定会履行,且必定会完毕,在完毕时就开释闭包中捕获的目标。所以在运用 PromiseKit 的 API 时彻底不必担心闭包对外部目标会造成循环引证问题,Promise 完毕时天然就开释了。当然要是自己供给的闭包,则仍是要小心考虑。
Promise 会在完毕的时分才会对闭包捕获的目标开释,这意味着若 Promise 闭包中传入 self,它会在 Promise 完毕时才会开释,即便 self 指向的目标早就该完毕生命周期了。例如:在 VC 顶用 Promise 封装一个网络恳求,网络恳求在 30s 后才抵达,然后刷新 VC 的 UI。但假如 VC 在网络呼应还未抵达前就关掉了页面,按理 VC 相关的逻辑也应相应停止。这时假如 Promise 闭包中传入 self,且 Promise 目标也被 self 或其他生命周期更长目标持有,VC 的生命周期将延伸到 Promise 完毕时,即网络恳求呼应抵达时。明显这样不符合预期,这时咱们才需求在 Promise 闭包运用 [weak self],弱引证捕获 self,这样在网络恳求呼应抵达时,self 不会被 Promise 延伸生命周期,就天然停止了 self 相关的办法调用。