PromiseKit 咱们都在项目中见过,是典型的一看就会,一用就废的技能。写 PromiseKit 代码想必咱们都经历过如下图的过错分配的惊骇。

img

经过本文,让咱们深入浅出重新认识 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 目标。供给了 fulfillreject 办法标记成功和失利的成果。

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

两者的差异能够用图表明为:

PromiseKit 教程

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 运用的难点在于结构和拼接。运用图来表明:

写成代码:

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 目标的时分要对应上,否则呈现编译过错。
  • donegettap 原语中的闭包无需回来值;但 thenmaprecover 原语都需在 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 中处理。
  • 要一起处理过错和成功值,运用 ensurefinally 尽管语义相似,但不能接纳上一步的成果。
  • 运用 whenrace 处理多个 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 相关的办法调用。