作者:Soroush Khanlou,原文链接,原文日期:2016/8/8
译者:Cwift;校对:Crystal Sun;定稿:CMB

译者注:英文原文发布时间较早,故原文代码中的 Swift 版本较旧,但是作者已将 GitHub 上的 Promise 示例代码更新到了最新 Swift 版本,所以译者在翻译本文时,将文章里的代码按照 GitHub 上的示例代码进行了替换,更新成了最新版本的 Swift 代码。

上周,我写了一篇介绍 Promise 的文章,Promise 是处理异步操作的高阶模块。只需要使用 fulfill()reject()then() 等函数,就可以简单自由地构建大量的功能。本文会展示我在 Promise 方面的一些探索。

Promise.all

Promise.all 是其中的典型,它保存所有异步回调的值。这个静态函数的作用是等待所有的 Promise 执行 fulfill(履行) ,一旦全部执行完毕,Promise.all 会使用所有履行后的值组成的数组对自己执行 fulfill。例如,你可能想在代码中对数组中的每个元素打点以捕获某个 API 的完成状态。使用 mapPromise.all 很容易实现:


let userPromises = users.map({ user in
    APIClient.followUser(user)
})
Promise.all(userPromises).then({
    //所有的用户都已经执行了 follow!
}).catch({ error in
    //其中一个 API 失败了。
})

要使用 Promise.all,需要首先创建一个新的 Promise,它代表所有 Promise 的组合状态,如果参数中的数组为空,可以立即执行 fulfill。


public static func all<T>(_ promises: [Promise<T>]) -> Promise<[T]> {
    return Promise<[T]>(work: { fulfill, reject in
        guard !promises.isEmpty else { fulfill([]); return }
            
    })
}

在这个 Promise 内部,遍历每个子 Promise,并分别为它们添加成功和失败的处理流程。一旦有子 Promise 执行失败了,就可以拒绝高阶的 Promise。


for promise in promises {
    promise.then({ value in

    }).catch({ error in
        reject(error)
    })
}

只有当所有的 Promise 都执行成功,才可以 fulfill 高阶的 Promise。检查一下以确保没有一个 Promise 被拒绝或者挂起,使用一点点 flatMap 的魔法,就可以对 Promise 的组合执行 fulfill 操作了。完整的方法如下:


public static func all<T>(_ promises: [Promise<T>]) -> Promise<[T]> {
        return Promise<[T]>(work: { fulfill, reject in
            guard !promises.isEmpty else { fulfill([]); return }
            for promise in promises {
                promise.then({ value in
                    if !promises.contains(where: { $0.isRejected || $0.isPending }) {
                        fulfill(promises.flatMap({ $0.value }))
                    }
                }).catch({ error in
                    reject(error)
                })
            }
        })
    }

请注意,Promise 只能履行或者拒绝一次。如果第二次调用 fulfill 或者 reject,不会对 Promise 的状态造成任何影响。

因为 Promise 是状态机,它保存了与完成度有关的重要状态。它是一种不同于 NSOperation 的方法。虽然 NSOperation 拥有一个完成回调以及操作的状态,但它不能保存得到的值,你需要自己去管理。

NSOperation 还持有线程模型以及优先级顺序相关的数据,而 Promise 对代码 如何 完成不做任何保证,只设置 完成后 需要执行的代码。Promise 类的定义足以证明。它唯一的实例变量是 state,状态包括挂起、履行或者拒绝(以及对应的数据),此外还有一个回调数组。(它还包含了一个隔离队列,但那不是真正的状态。)

delay

有一种很有用的 Promise 可以延迟执行自己的操作。


public static func delay(_ delay: TimeInterval) -> Promise<()> {
    return Promise<()>(work: { fulfill, reject in
        DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
            fulfill(())
        })
    })
}

在方法内部,可以使用 usleep 或者其他方法来实现延迟,不过 asyncAfter 方法足够简单。当构建其他有趣的 Promise 时,这个延迟 Promise 会很有用。

timeout

接下来,使用 delay 来构建 timeout。该 Promise 如果超过一定时间就会被拒绝。


public static func timeout<T>(_ timeout: TimeInterval) -> Promise<T> {
    return Promise<T>(work: { fulfill, reject in
        delay(timeout).then({ _ in
            reject(NSError(domain: "com.khanlou.Promise", code: -1111, userInfo: [ NSLocalizedDescriptionKey: "Timed out" ]))
        })
    })
}

这个 Promise 自身没有太多用处,但它可以帮助我们构建一些其他功能的 Promise。

race

Promise.racePromise.all 的小伙伴,它不需要等待所有的子 Promise 完成,它只履行或者拒绝第一个完成的 Promise。


public static func race<T>(_ promises: [Promise<T>]) -> Promise<T> {
    return Promise<T>(work: { fulfill, reject in
        guard !promises.isEmpty else { fatalError() }
        for promise in promises {
            promise.then(fulfill, reject)
        }
    })
}

因为 Promise 只能被执行或拒绝一次,所以当移除了 .pending 的状态后,在外部对 Promise 调用 fulfill 或者 reject 不会产生任何影响。

有了这个函数,使用 timeoutPromise.race 可以创建一个新的 Promise,针对成功、失败或者超过了规定时间三种情况。把它定义在 Promise 的扩展中。


public func addTimeout(_ timeout: TimeInterval) -> Promise<Value> {
    return Promise.race(Array([self, Promise<Value>.timeout(timeout)]))
}

可以在正常的 Promise 链中使用它,像下面这样:


APIClient
    .getUsers()
    .addTimeout(0.5)
    .then({
        //在 0.5 秒内获取了用户数据
    })
    .catch({ error in
        //也许是超时引发的错误,也许是网络错误
    })

这是我喜欢 Promise 的原因之一,它们的可组合性使得我们可以轻松地创建各种行为。通常需要保证 Promise 在 某个时刻 被履行或者拒绝,但是 timeout 函数允许我们用常规的方式来修正这种行为。

recover

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


extension Promise {
    public func recover(_ recovery: @escaping (Error) throws -> Promise<Value>) -> Promise<Value> {
    
    }
}

在方法体中,需要返回一个新的 Promise,如果当前的 Promise(self)执行成功,需要把成功状态转移给新的 Promise。


public func recover(_ recovery: @escaping (Error) throws -> Promise<Value>) -> Promise<Value> {
    return Promise(work: { fulfill, reject in
        self.then(fulfill).catch({ error in
        
        })
    })
}

然而,catch 是另一回事了。如果 Promise 执行失败,应该调用提供的 recovery 函数。该函数会返回一个新的 Promise。无论 recovery 中的 Promise 执行成功与否,都要把结果返回给新的 Promise。


//...
do {
    try recovery(error).then(fulfill, reject)
} catch (let error) {
    reject(error)
}
//...

完整的方法如下:


public func recover(_ recovery: @escaping (Error) throws -> Promise<Value>) -> Promise<Value> {
    return Promise(work: { fulfill, reject in
        self.then(fulfill).catch({ error in
            do {
                try recovery(error).then(fulfill, reject)
            } catch (let error) {
                reject(error)
            }
        })
    })
}

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


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)
    })
}
  • 如果数量不足 1,直接生成 Promise 并返回。

  • 否则,创建一个包含了需要重试的 Promise 的新的 Promise,如果失败了,在 delay 时间之后恢复到之前的状态并重试,不过此时的重试次数减为 count - 1

基于之前编写的 delayrecover 函数构建了重试的函数。

在上面的这些例子中,轻量且可组合的部分组合在一起,就得到了简单优雅的解决方案。所有的这些行为都是建立在 Promise 核心代码所提供的简单的 .thencatch 函数上的。通过格式化完成闭包的样式,可以解决诸如超时、恢复、重试以及其他可以通过简单可重用的方式解决的问题。这些例子仍然需要一些测试和验证,我会在未来一段时间内慢慢地添加到 GitHub 仓库 中。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg


SwiftGG翻译组
1.6k 声望957 粉丝

走心的 Swift 翻译组


引用和评论

0 条评论