Promise 的幾種通用模式

作者:Soroush Khanlou讥此,原文鏈接,原文日期:2016/8/8
譯者:Cwift;校對:Crystal Sun花吟;定稿:CMB

譯者注:英文原文發(fā)布時(shí)間較早,故原文代碼中的 Swift 版本較舊厨姚,但是作者已將 GitHub 上的 Promise 示例代碼更新到了最新 Swift 版本衅澈,所以譯者在翻譯本文時(shí),將文章里的代碼按照 GitHub 上的示例代碼進(jìn)行了替換谬墙,更新成了最新版本的 Swift 代碼今布。

上周,我寫了一篇介紹 Promise 的文章拭抬,Promise 是處理異步操作的高階模塊部默。只需要使用 fulfill()reject()then() 等函數(shù)造虎,就可以簡單自由地構(gòu)建大量的功能傅蹂。本文會展示我在 Promise 方面的一些探索。

Promise.all

Promise.all 是其中的典型累奈,它保存所有異步回調(diào)的值贬派。這個(gè)靜態(tài)函數(shù)的作用是等待所有的 Promise 執(zhí)行 fulfill(履行) ,一旦全部執(zhí)行完畢澎媒,Promise.all 會使用所有履行后的值組成的數(shù)組對自己執(zhí)行 fulfill搞乏。例如,你可能想在代碼中對數(shù)組中的每個(gè)元素打點(diǎn)以捕獲某個(gè) API 的完成狀態(tài)戒努。使用 mapPromise.all 很容易實(shí)現(xiàn):

let userPromises = users.map({ user in
    APIClient.followUser(user)
})
Promise.all(userPromises).then({
    //所有的用戶都已經(jīng)執(zhí)行了 follow请敦!
}).catch({ error in
    //其中一個(gè) API 失敗了镐躲。
})

要使用 Promise.all,需要首先創(chuàng)建一個(gè)新的 Promise侍筛,它代表所有 Promise 的組合狀態(tài)萤皂,如果參數(shù)中的數(shù)組為空,可以立即執(zhí)行 fulfill匣椰。

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

在這個(gè) Promise 內(nèi)部裆熙,遍歷每個(gè)子 Promise,并分別為它們添加成功和失敗的處理流程禽笑。一旦有子 Promise 執(zhí)行失敗了入录,就可以拒絕高階的 Promise。

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

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

只有當(dāng)所有的 Promise 都執(zhí)行成功佳镜,才可以 fulfill 高階的 Promise僚稿。檢查一下以確保沒有一個(gè) Promise 被拒絕或者掛起,使用一點(diǎn)點(diǎn) flatMap 的魔法蟀伸,就可以對 Promise 的組合執(zhí)行 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 只能履行或者拒絕一次啊掏。如果第二次調(diào)用 fulfill 或者 reject蠢络,不會對 Promise 的狀態(tài)造成任何影響。

因?yàn)?Promise 是狀態(tài)機(jī)脖律,它保存了與完成度有關(guān)的重要狀態(tài)谢肾。它是一種不同于 NSOperation 的方法。雖然 NSOperation 擁有一個(gè)完成回調(diào)以及操作的狀態(tài)小泉,但它不能保存得到的值芦疏,你需要自己去管理。

NSOperation 還持有線程模型以及優(yōu)先級順序相關(guān)的數(shù)據(jù)微姊,而 Promise 對代碼 如何 完成不做任何保證酸茴,只設(shè)置 完成后 需要執(zhí)行的代碼。Promise 類的定義足以證明兢交。它唯一的實(shí)例變量是 state薪捍,狀態(tài)包括掛起、履行或者拒絕(以及對應(yīng)的數(shù)據(jù))配喳,此外還有一個(gè)回調(diào)數(shù)組酪穿。(它還包含了一個(gè)隔離隊(duì)列,但那不是真正的狀態(tài)晴裹。)

delay

有一種很有用的 Promise 可以延遲執(zhí)行自己的操作被济。

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

在方法內(nèi)部,可以使用 usleep 或者其他方法來實(shí)現(xiàn)延遲涧团,不過 asyncAfter 方法足夠簡單只磷。當(dāng)構(gòu)建其他有趣的 Promise 時(shí)经磅,這個(gè)延遲 Promise 會很有用。

timeout

接下來钮追,使用 delay 來構(gòu)建 timeout预厌。該 Promise 如果超過一定時(shí)間就會被拒絕。

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" ]))
        })
    })
}

這個(gè) Promise 自身沒有太多用處元媚,但它可以幫助我們構(gòu)建一些其他功能的 Promise轧叽。

race

Promise.racePromise.all 的小伙伴,它不需要等待所有的子 Promise 完成惠毁,它只履行或者拒絕第一個(gè)完成的 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)
        }
    })
}

因?yàn)?Promise 只能被執(zhí)行或拒絕一次,所以當(dāng)移除了 .pending 的狀態(tài)后鞠绰,在外部對 Promise 調(diào)用 fulfill 或者 reject 不會產(chǎn)生任何影響。

有了這個(gè)函數(shù)飒焦,使用 timeoutPromise.race 可以創(chuàng)建一個(gè)新的 Promise蜈膨,針對成功、失敗或者超過了規(guī)定時(shí)間三種情況牺荠。把它定義在 Promise 的擴(kuò)展中翁巍。

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 秒內(nèi)獲取了用戶數(shù)據(jù)
    })
    .catch({ error in
        //也許是超時(shí)引發(fā)的錯(cuò)誤休雌,也許是網(wǎng)絡(luò)錯(cuò)誤
    })

這是我喜歡 Promise 的原因之一灶壶,它們的可組合性使得我們可以輕松地創(chuàng)建各種行為。通常需要保證 Promise 在 某個(gè)時(shí)刻 被履行或者拒絕杈曲,但是 timeout 函數(shù)允許我們用常規(guī)的方式來修正這種行為驰凛。

recover

recover 是另一個(gè)有用的函數(shù)。它可以捕獲一個(gè)錯(cuò)誤担扑,然后輕松地恢復(fù)狀態(tài)恰响,同時(shí)不會弄亂其余的 Promise 鏈。
我們很清楚這個(gè)函數(shù)的形式:它應(yīng)該接受一個(gè)函數(shù)涌献,該函數(shù)中接受錯(cuò)誤并返回新的 Promise胚宦。recover 方法也應(yīng)該返回一個(gè) Promise 以便繼續(xù)鏈接 Promise 鏈。

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

在方法體中燕垃,需要返回一個(gè)新的 Promise枢劝,如果當(dāng)前的 Promise(self)執(zhí)行成功,需要把成功狀態(tài)轉(zhuǎn)移給新的 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 執(zhí)行失敗印叁,應(yīng)該調(diào)用提供的 recovery 函數(shù)被冒。該函數(shù)會返回一個(gè)新的 Promise军掂。無論 recovery 中的 Promise 執(zhí)行成功與否,都要把結(jié)果返回給新的 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)
            }
        })
    })
}

有了這個(gè)新的函數(shù)就可以從錯(cuò)誤中恢復(fù)蝗锥。例如,如果網(wǎng)絡(luò)沒有加載我們期望的數(shù)據(jù)率触,可以從緩存中加載數(shù)據(jù):

APIClient.getUsers()
    .recover({ error in 
        return cache.getUsers()
    }).then({ user in
        //更新 UI
    }).catch({ error in
        //錯(cuò)誤處理
    })

retry

重試是我們可以添加的另一個(gè)功能终议。若要重試,需要指定重試的次數(shù)以及一個(gè)能夠創(chuàng)建 Promise 的函數(shù)葱蝗,該 Promise 包含了重試要執(zhí)行的操作(所以這個(gè) Promise 會被重復(fù)創(chuàng)建很多次)穴张。

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)
    })
}
  • 如果數(shù)量不足 1,直接生成 Promise 并返回两曼。
  • 否則皂甘,創(chuàng)建一個(gè)包含了需要重試的 Promise 的新的 Promise,如果失敗了悼凑,在 delay 時(shí)間之后恢復(fù)到之前的狀態(tài)并重試偿枕,不過此時(shí)的重試次數(shù)減為 count - 1

基于之前編寫的 delayrecover 函數(shù)構(gòu)建了重試的函數(shù)户辫。

在上面的這些例子中渐夸,輕量且可組合的部分組合在一起,就得到了簡單優(yōu)雅的解決方案渔欢。所有的這些行為都是建立在 Promise 核心代碼所提供的簡單的 .thencatch 函數(shù)上的墓塌。通過格式化完成閉包的樣式,可以解決諸如超時(shí)奥额、恢復(fù)苫幢、重試以及其他可以通過簡單可重用的方式解決的問題。這些例子仍然需要一些測試和驗(yàn)證披坏,我會在未來一段時(shí)間內(nèi)慢慢地添加到 GitHub 倉庫 中态坦。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán)棒拂,最新文章請?jiān)L問 http://swift.gg伞梯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帚屉,隨后出現(xiàn)的幾起案子谜诫,更是在濱河造成了極大的恐慌,老刑警劉巖攻旦,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喻旷,死亡現(xiàn)場離奇詭異,居然都是意外死亡牢屋,警方通過查閱死者的電腦和手機(jī)且预,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門槽袄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锋谐,你說我怎么就攤上這事遍尺。” “怎么了涮拗?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵乾戏,是天一觀的道長。 經(jīng)常有香客問我三热,道長鼓择,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任就漾,我火速辦了婚禮呐能,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘从藤。我一直安慰自己催跪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布夷野。 她就那樣靜靜地躺著,像睡著了一般荣倾。 火紅的嫁衣襯著肌膚如雪悯搔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天舌仍,我揣著相機(jī)與錄音妒貌,去河邊找鬼。 笑死铸豁,一個(gè)胖子當(dāng)著我的面吹牛灌曙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播节芥,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼在刺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了头镊?” 一聲冷哼從身側(cè)響起蚣驼,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎相艇,沒想到半個(gè)月后颖杏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坛芽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年留储,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了翼抠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡获讳,死狀恐怖阴颖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赔嚎,我是刑警寧澤膘盖,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站尤误,受9級特大地震影響侠畔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜损晤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一软棺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尤勋,春花似錦喘落、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至暖哨,卻和暖如春赌朋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背篇裁。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工沛慢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人达布。 一個(gè)月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓团甲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親黍聂。 傳聞我的和親對象是個(gè)殘疾皇子躺苦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容