PromiseKit 详解

1,451 阅读7分钟

一些基本概念

在讲解PromiseKit 之前我们先来梳理一些基础概念:

同步和异步

这是一个老生常谈的问题, 同步操作意味着在操作完成之前,运行这个操作的线程都将被占用,直到函数最终被抛出或者返回。 截屏2024-07-19 15.28.51.png

尤其在 iOS 开发中,我们使用的UIKit并不是线程安全的:对用户输入的处理和 UI 的绘制,必须在与主线程绑定的 main runloop 中进行。假设我们希望用户界面以每秒 60 帧的速率运行,那么主线程中每两次绘制之间,所能允许的处理时间最多只有 16 毫秒 (1 / 60s)。当主线程中要同步处理的其他操作耗时很少时,这不会造成什么问题。但是,如果这个同步操作耗时过长的话,主线程将被阻塞。它不能接受用户输入,也无法向 GPU 提交请求去绘制新的 UI,这将导致用户界面掉帧甚至卡死。

而异步操作调用者不会立刻得到结果,两者之间主要的区别在于消息的通知机制 所以在开发过程中我们经常会遇到一些需要异步处理的相关业务,比如网络请求,文件io,还有一些耗时计算操作,通常在iOS中会这样处理异步逻辑,将耗时操作扔到子线程模块,在回调的时候提供运行在主线程的回调,以供 UI 操作之用

func myFunction(_ completion: @escaping (String?, Error?) -> Void) {
    DispatchQueue.global().async {
        do {
            let d = try Data(contentsOf: URL(string: "")!) // 极其耗时
            DispatchQueue.main.async {
                completion(String(data: d, encoding: .utf8), nil)
            }
        } catch {
            DispatchQueue.main.async {
                completion(nil, error)
            }
        }
    }
}

当然这种方式有着明显的弊端:

  1. 错误处理隐藏在回调函数的参数中,无法用 throw 的方式明确地告知并强制调用侧去进行错误处理。下游只能通过 if error 是否不为空
  2. 对回调函数的调用没有编译器保证,开发者可能会忘记调用 completion,或者多次调用 completion。
  3. 通过 DispatchQueue 进行线程调度很快会使代码复杂化。特别是如果线程调度的操作被隐藏在被调用的方法中的时候,不查看源码的话,在 (调用侧的) 回调函数中,几乎无法确定代码当前运行的线程状态。
  4. 对于正在执行的任务,没有很好的取消机制。

再举一个例子:这是一段常见的业务逻辑:

APIClient.fetchCurrentUser(success: { currentUser **in**
    APIClient.fetchFollowers(user: currentUser, success: { followers **in**
        // 现在你得到了一个 followers 数组
    }, failure: { error **in**
        // 错误处理
    })
}, failure: { error **in**
    // 错误处理
})

我们发现在经过第一层网络请求之后,我们又触发了二次的网络请求,且第二次依赖于第一次的返回结果,倘若我们再获取到第二次网络请求的结果还要进行第三次,我们要继续我们的嵌套结构,第四次第五次依次类推,很显然这种嵌套地狱是我们无法忍受的,而且代码可读性会变的异常糟糕,当然iOS 中提供了一些NSOPeration or GCD的解法,但是很显然逻辑不够紧凑,而且我的参数处理起来也是会有一些麻烦,所以PromiseKit 提供了很好的解决方案,像上面的例子在Primise中可以这样去实现

APIClient.fetchCurrentUser().then { currentUser in
    return APIClient.fetchFollowers(user: currentUser)
}.done { followers in
    // 你现在有了一个关注者数组
    print("Followers: \(followers.map { $0.name })")
}.catch { error in
    // 出现错误时执行
    print("Error: \(error)")
}

我们来实现一下 fetchCurrentUser和 fetchFollowers ,实现就变成了这样:

static func fetchCurrentUser() -> Promise<User> {
    return Promise { seal in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
            // 模拟成功获取当前用户
            let currentUser = User(id: 1, name: "John Doe")
            seal.fulfill(currentUser)
            // 或者模拟一个错误 
            // seal.reject(Error(domain: "com.example.error", code: 404, userInfo: nil))
        }
    }
}

static func fetchFollowers(user: User) -> Promise<[Follower]> {
    return Promise { seal in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
            // 模拟成功获取关注者列表
            let followers = [
                Follower(id: 2, name: "Jane Doe"),
                Follower(id: 3, name: "Jim Beam"),
                Follower(id: 4, name: "Jack Daniels")
            ]
            seal.fulfill(followers)
            // 或者模拟一个错误 
            // seal.reject(Error(domain: "com.example.error", code: 404, userInfo: nil))
        }
    }
}

我们要实现Promise的链式调用,原函数自然应该返回一个promise,这有点像flutter中的future 很显然这样的写法没有了前面的嵌套地狱,Promise 是将嵌套/缩进样式的代码变成一个层级的代码,于此同时我们注意到多了很多的陌生的名词和语法糖:Promise Then catch seal fulfill reject,下面我们来展开分析一下

在Promise的核心实现中,主要有两大核心部分,分别是个Box 和 Resolver image.png

  • Box:表示一个容器,包含了 执行状态执行结果回调任务列表

  • Resolver执行器。 Promise 通过 Sealant 枚举类型将三种执行状态(pendingfulfilledrejected)分成两种执行状态:

  • 开始状态pending 状态。

  • 结束状态resolved 状态,具体可以是 fulfilled 或 rejected

Sealant 的定义如下所示:

// 枚举 Sealant,用于表示 Promise 的状态
enum Sealant<R> {
    case pending(Handlers<R>)
    case resolved(R)
}

final class Handlers<R> {
    var bodies: [(R) -> Void] = []
    func append(_ item: @escaping(R) -> Void) { bodies.append(item) }
}

其中,pending 状态的关联值存储了 回调任务列表 Handlersresolved 状态的关联值存储了两种细分的状态 Result

Result 枚举类型则用于进一步表示 fulfilled 和 rejected 状态,具体定义如下所示:

public enum Result<T> {
    case fulfilled(T)
    case rejected(Error)
}

fulfill 、reject 、peding

image.png

fulfill 、reject 、peding分别用于表示 Promise 的三种状态, Promise 内部的状态由 执行器(executor)  或 解析器(resolver)  来进行更新。Promise 创建时的状态默认为 pending,用户为 Promise 提供状态转移逻辑,比如:网络请求成功时将状态设置为 fulfilled,网络请求失败时将状态设置为 rejected。通常,执行器会提供两个方法 resolve 和 reject 分别用于设置 fulfilled 和 rejected 状态。

Box 抽象类定义了三个方法,分别是:

class Box<T> {
    func inspect() -> Sealant<T> { fatalError() }
    func inspect(_: (Sealant<T>) -> Void) { fatalError() }
    func seal(_: T) {}
}

inspect() 方法用于检查内部状态,返回值为 Sealant 值。对于 SealedBox,其返回始终为 resolved<T>

inspect(_ body: (Sealant<T>) -> Void) 方法将内部结果作为参数传递给闭包并执行。

seal(_: T) 方法非常关键,当 Box 为 pending 状态时,seal 方法可以将内部状态更新为 resolved,同时执行 回调任务列表** 中的所有任务。Resolver 就是通过 seal 方法来更新状态的。具体如下所示:

class EmptyBox<T>: Box<T> {
    ...

    override func seal(_ value: T) {
        var handlers: Handlers<T>!
        barrier.sync(flags: .barrier) {
            guard case .pending(let _handlers) = self.sealant else {
                return  // already fulfilled!
            }
            handlers = _handlers
            self.sealant = .resolved(value)
        }

        if let handlers = handlers {
            handlers.bodies.forEach{ $0(value) }
        }
    }
}

Resolver

Resolver 的核心作用是更新执行状态和执行结果,并执行回调任务列表中的任务。由于 Box 封装了执行状态、执行结果、回调任务列表,并且提供了更新状态的方法 seal。因此,Resolver 只需提供针对不同状态的便利方法,内部调用 Box 的 seal 方法进行更新即可。具体如下所示。

public final class Resolver<T> {
    let box: Box<Result<T>>

    init(_ box: Box<Result<T>>) {
        self.box = box
    }

    deinit {
        if case .pending = box.inspect() {
            conf.logHandler(.pendingPromiseDeallocated)
        }
    }
}

public extension Resolver {
    /// Fulfills the promise with the provided value
    func fulfill(_ value: T) {
        box.seal(.fulfilled(value))
    }

    /// Rejects the promise with the provided error
    func reject(_ error: Error) {
        box.seal(.rejected(error))
    }

    /// Resolves the promise with the provided result
    func resolve(_ result: Result<T>) {
        box.seal(result)
    }
    ...   
}

此外,Promise 还支持通过链式操作符实现回调任务的链式执行,其原理是在内部维护一个回调任务列表,当 Promise 到达结束状态时,自动执行内部的回调任务,从而整体实现异步任务的链式执行。

Then

  • 简单来讲,then 方法就是把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
  • 而 Promise 的优势就在于这个链式调用。我们可以在 then 方法中继续写 Promise 对象并返回,然后继续调用 then 来进行回调操作。 我们来看一下这个Then的内部实现:
// 定义 Result 枚举,用于表示 Promise 的结果
public enum Result<T> {
    case fulfilled(T)
    case rejected(Error)
}

/// Thenable represents an asynchronous operation that can be chained.
public protocol Thenable: AnyObject {
    associatedtype T
    /// `pipe` is immediately executed when this `Thenable` is resolved
    func pipe(to: escaping (Result<T>) -> Void)
    /// The resolved result or nil if pending.
    var result: Result<T>? { get }
}

/// -  See: `Thenable.pipe`
public func pipe(to: escaping(Result<T>) -> Void) {
    switch box.inspect() {
    case .pending:
        box.inspect {
            switch $0 {
            case .pending(let handlers):
                handlers.append(to)
            case .resolved(let value):
                to(value)
            }
        }
    case .resolved(let value):
        to(value)
    }
}
从实现中可以看到,`pipe(to:)` 方法会先判断 `Box` 的状态,如果是 `pending` 状态,则将闭包加入回调任务列表;如果是 `resolved` 状态,则立即执行闭包

func then<U: Thenable>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil _ body: escaping (T) throws -> U) -> Promise<U.T> {
    let rp = Promise<U.T>(.pending)
    pipe {
        switch $0 {
        case .fulfilled(let value):
            on.async(flags: flags) {
                do {
                    let rv = try body(value)
                    guard rv !== rp else { throw PMKError.returnedSelf }
                    rv.pipe(to: rp.box.seal)
                } catch {
                    rp.box.seal(.rejected(error))
                }
            }
        case .rejected(let error):
            rp.box.seal(.rejected(error))
        }
    }
    return rp
 }

PromiseKit 相关使用

when() 方法

  • when 方法提供了并行执行异步操作的能力,并且只有在所有异步操作执行完后才执行回调。
  • 和其他的 promise 链一样,when 方法中任一异步操作发生错误,都会进入到下一个 catch 方法中。
when(resolved: promise1, promise2, promise3).then { results in
     for result in results where case .fulfilled(let value) {
        //…
     }
 }.catch { error in
     // invalid! Never rejects
 }

当我们需要批量处理一些异步逻辑,这是一个非常常见的场景,当然我们可以通过NSOperation 去实现。虽然 NSOperation 拥有一个完成回调以及操作的状态,但它不能保存得到的值,你需要自己去管理。而且 NSOperation 还持有线程模型以及优先级顺序相关的数据,而 Promise 对代码如何完成不做任何保证,

race() 方法

  • race 按字面解释,就是赛跑的意思。race 的用法与 when 一样,只不过 when 是等所有异步操作都执行完毕后才执行 then 回调。而 race 的话只要有一个异步操作执行完毕,就立刻执行 then 回调。 举个常见的例子,我们经常会判断一些业务引擎是否启动超时,亦或者我们要拿到率先完成的异步操作,并获取到结果 我们就可以用到race:
race(promise1(), promise2()).done{ data in
   print("结果:(data)")
}

recover() 方法

recover 是另一个有用的函数。它可以捕获一个错误,然后轻松地恢复状态,同时不会弄乱其余的 Promise 链。 我们很清楚这个函数的形式:它应该接受一个函数,该函数中接受错误并返回新的 Promise。recover 方法也应该返回一个 Promise 以便继续链接 Promise 链。

有了这个新的函数就可以从错误中恢复。例如,如果网络没有加载我们期望的数据,可以从缓存中加载数据:

APIClient.getUsers()
    .recover({ error in 
        return cache.getUsers()
    }).then({ user in
        //更新 UI
    }).catch({ error in
        //错误处理
    })

当然我们也可以自行扩展retry逻辑: 重试是我们可以添加的另一个功能。若要重试,需要指定重试的次数以及一个能够创建 Promise 的函数,该 Promise 包含了重试要执行的操作(所以这个 Promise 会被重复创建很多次)。

public static func retry<T>(count: Int, delay: TimeInterval, generate: @escaping () -> Promise<T>) -> Promise<T> {
    if count <= 0 {
        return generate()
    }
    return Promise<T>(work: { fulfill, reject in
        generate().recover({ error in
            return self.delay(delay).then({
                return retry(count: count-1, delay: delay, generate: generate)
            })
        }).then(fulfill).catch(reject)
    })
}

after() 方法

我们经常会对代码逻辑进行一些延迟执行 通过gcd的相关api当然可以做到,当然PromiseKit提供了更简洁的方式来处理:

after(seconds: 5).done {
    print``("欢迎访问hangge.com")
}

总结:

Promise 是异步任务的执行过程,表示一个值的生产过程,Promise中还有很多操作值得去发现和扩展,可以去 github.com/mxcl/Promis… 仔细研究

参考链接

www.hangge.com/blog/cache/…

segmentfault.com/a/119000000…

chuquan.me/2022/10/29/…

khanlou.com/2016/08/com…