Promise 的几种通用模式

作者: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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容