全部文章
以下是對(duì)
PromiseKit的README.md的翻譯。
then and done
下面是一個(gè)典型的 promise 調(diào)用鏈:
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}
如果改為使用完成處理程序來實(shí)現(xiàn),則會(huì)像下面一樣:
login { creds, error in
if let creds = creds {
fetch(avatar: creds.user) { image, error in
if let image = image {
self.imageView = image
}
}
}
}
then 看起來只是實(shí)現(xiàn)完成處理程序的另一種實(shí)現(xiàn),但是它可以提供的更多。當(dāng)我們開始去理解代碼的時(shí)候,它的可讀性更高。上面的 promise 鏈非常容易掃視并且理解:一個(gè)異步操作接著另一個(gè),一行接著一行。對(duì)于現(xiàn)階段的 swift,它非常接近于過程代碼。
done 和 then 一樣,但是不能再返回一個(gè) promise 了。它經(jīng)常用在調(diào)用鏈的末尾,用于處理成功的情況。上面代碼中,你可以發(fā)現(xiàn)在 done 中,我們拿到了最終的 image,并用它來設(shè)置 UI。
我們來對(duì)比一下兩種實(shí)現(xiàn) login 的方法:
func login() -> Promise<Creds>
// Compared with:
func login(completion: (Creds?, Error?) -> Void)
// ^^ ugh. Optionals. Double optionals.
使用 promise 的差別在于,你的方法需要返回一個(gè) promise,而不是接收并執(zhí)行一個(gè)回調(diào)。在鏈上的每個(gè)處理程序都要返回一個(gè) promise。Promise 類定義了 then 方法,他會(huì)在繼續(xù)執(zhí)行鏈之前等待給定的 promise 執(zhí)行完成。在程序上,調(diào)用鏈一次只能執(zhí)行一個(gè) promise。
一個(gè) Promise 代表了一個(gè)異步執(zhí)行的結(jié)果。他有一個(gè)類型來表示它包含的類的類型。例如,在上面的例子中,login 是一個(gè)返回代表 Creds 實(shí)例的 Promise 的方法。
注意:
done是 PromiseKit 5 中的新功能。我們之前定義了一個(gè)then的變體,它允許你不返回一個(gè) Promise。不幸的是,這個(gè)約定經(jīng)常使得 Swift 混淆,并且會(huì)導(dǎo)致一些奇怪的、難以排查的錯(cuò)誤信息。它也使得 PromiseKit 難以使用。done的引入,使我們?cè)趯?promise 鏈時(shí),不需要再添加額外的類型信息,來用于幫助編譯器推斷類型信息了。
你可能注意到了,這和使用 completion 回調(diào)的方式有所不同,它忽略了 error。事實(shí)上并非如此,相反,在錯(cuò)誤處理方法,Promise 更加的方便,而且很難忽略。
catch
在 Promise 中,異常會(huì)沿著 Promise 鏈傳遞,這可以確保你的 app 的健壯性,而且代碼清晰。
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch {
// any errors in the whole chain land here
}
如果你忘記了在 chain 中使用
catch,swift 將會(huì)發(fā)出警告。我們將會(huì)在稍后介紹更多細(xì)節(jié)。
每個(gè) promsie 類代表著單個(gè)異步任務(wù)。如果一個(gè)任務(wù)失敗,則對(duì)應(yīng)的 Promise 將會(huì)變成 rejected 狀態(tài)。在鏈中,一旦一個(gè) promise 變成 rejected 狀態(tài),它將忽略之后的 then,直接去執(zhí)行下一個(gè) catch。(嚴(yán)格來說,他會(huì)執(zhí)行所所有繼的 catch 處理程序。)
我們來對(duì)比一下這種情況和它對(duì)應(yīng)的使用 completion 回調(diào)的處理程序:
func handle(error: Error) {
//…
}
login { creds, error in
guard let creds = creds else { return handle(error: error!) }
fetch(avatar: creds.user) { image, error in
guard let image = image else { return handle(error: error!) }
self.imageView.image = image
}
}
在可讀性方面,上面的代碼雖然使用了 guard 和一個(gè)固定的錯(cuò)誤處理程序,和 Promise 鏈比起來,不值一提。
ensure
我們已將學(xué)會(huì)了寫一個(gè)異步任務(wù),接下來,我們來擴(kuò)展語(yǔ)法:
firstly {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
return login()
}.then {
fetch(avatar: $0.user)
}.done {
self.imageView = $0
}.ensure {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
//…
}
不管你的調(diào)用鏈的結(jié)果怎樣——成功還是失敗—— ensure 處理程序?qū)⑹冀K會(huì)被調(diào)用。
我們來對(duì)比一下這種模式和它對(duì)應(yīng)的 completion 回調(diào)的方式:
UIApplication.shared.isNetworkActivityIndicatorVisible = true
func handle(error: Error) {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
//…
}
login { creds, error in
guard let creds = creds else { return handle(error: error!) }
fetch(avatar: creds.user) { image, error in
guard let image = image else { return handle(error: error!) }
self.imageView.image = image
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
}
在修改這段代碼時(shí),很容易就會(huì)忘記重置指示器,也就會(huì)導(dǎo)致 bug。在使用 Promise 時(shí),這種錯(cuò)誤基本上是不可能發(fā)生的。swift 編譯器將會(huì)強(qiáng)制你使用 Promise 來補(bǔ)充調(diào)用鏈。你基本不需要 review 提交的代碼。
注意:PromiseKit 曾經(jīng)幾次將這個(gè)功能反復(fù)的命名為
always或ensure。對(duì)此感到抱歉。
當(dāng) ensure 用于終止 promise 鏈并且沒有返回值時(shí),你也可以使用 finally 。
spinner(visible: true)
firstly {
foo()
}.done {
//…
}.catch {
//…
}.finally {
self.spinner(visible: false)
}
when
在使用 completion 回調(diào)時(shí),對(duì)多個(gè)異步操作的處理即慢又麻煩。慢是指它是依次處理的:
operation1 { result1 in
operation2 { result2 in
finish(result1, result2)
}
}
快速(并行)的代碼將會(huì)降低代碼的可讀性:
var result1: …!
var result2: …!
let group = DispatchGroup()
group.enter()
group.enter()
operation1 {
result1 = $0
group.leave()
}
operation2 {
result2 = $0
group.leave()
}
group.notify(queue: .main) {
finish(result1, result2)
}
使用 Promise 就非常簡(jiǎn)單:
firstly {
when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
//…
}
when 接收多個(gè) Promise,它將會(huì)等待傳入的 promise 執(zhí)行結(jié)束,并且返回一個(gè)包含這里 promsie 結(jié)果的 promise。
與其他的 Promise 鏈一樣,傳入的 promise 中任何一個(gè)失敗,調(diào)用鏈都會(huì)調(diào)用下一個(gè) catch。
PromiseKit Extensions
當(dāng)制作 PromiseKit 框架時(shí),我們發(fā)現(xiàn)只有在實(shí)現(xiàn)異步操作的時(shí)候,我們才會(huì)使用 Promise。因此,只要有可能,我們都對(duì)蘋果的 API 提供了符合 promise 的擴(kuò)展。例如:
firstly {
CLLocationManager.promise()
}.then { location in
CLGeocoder.reverseGeocode(location)
}.done { placemarks in
self.placemark.text = "\(placemarks.first)"
}
使用這些擴(kuò)展時(shí),需要添加下面的 subspecs:
pod "PromiseKit"
pod "PromiseKit/CoreLocation"
pod "PromiseKit/MapKit"
在 PromiseKit organization 中可以看到所有可用的擴(kuò)展。你可以點(diǎn)擊鏈接查看所有可用的內(nèi)容,也可以閱讀源碼和文檔。所有的文件和方法都有豐富的說明文檔。
我們也對(duì)通用的庫(kù)進(jìn)行了擴(kuò)展,例如:Alamofire。
創(chuàng)建 Promises
標(biāo)準(zhǔn)的擴(kuò)展可以應(yīng)對(duì)絕大部分的場(chǎng)景,但有時(shí)你還是得寫一些特殊的調(diào)用鏈。例如:你使用的第三方庫(kù)沒有提供 Poromise 的 API,或者你自己寫的一些異步功能。無論哪種情況,添加 promise 擴(kuò)展都非常的簡(jiǎn)單。當(dāng)你查看標(biāo)準(zhǔn)擴(kuò)展的源碼時(shí),你會(huì)發(fā)現(xiàn)它都是按照如下的方法來實(shí)現(xiàn)的。
假設(shè)我們有如下所示的方法:
func fetch(completion: (String?, Error?) -> Void)
我們應(yīng)該如何改造成 promise 類型的方法呢?非常簡(jiǎn)單:
func fetch() -> Promise<String> {
return Promise { fetch(completion: $0.resolve) }
}
下面的擴(kuò)展版本可讀性更高一些:
func fetch() -> Promise<String> {
return Promise { seal in
fetch { result, error in
seal.resolve(result, error)
}
}
}
Promise 初始化了一個(gè) seal 類給我我們,seal 不僅提供了一些處理常見結(jié)果的方法,甚至涵蓋了各種罕見的情況,因?yàn)槲覀兛梢暂p松的向現(xiàn)有代碼庫(kù)添加 promise 擴(kuò)展。
注意:我們嘗試著只用
Promise(fetch)來讓方法更簡(jiǎn)單,但是沒能做到,因?yàn)闀r(shí)常需要提供額外的信息,swift 編譯器才能順利的編譯。很抱歉,我們沒能做到這一點(diǎn)。
注意:在 PMK 4 中,初始化程序提供了兩個(gè)參數(shù)來處理不同的結(jié)果: fulfill 和 reject。在 PMK 5 和 6 中,只提供了包含 fulfill 和 reject 方法的一個(gè)類,但是它可以處理更多的情況。通常,你只需要將處理程序傳遞給 resolve 方法,然后讓 swift 根據(jù)具體情況來適配 resolve 的參數(shù)(如上所示)。
注意:Guarantees(下面會(huì)介紹)的初始化程序稍有不同,因?yàn)?Guarantees 只能處理成功的情況,所以初始化程序的閉包參數(shù)只是一個(gè)閉包,而不是 Resolver 對(duì)象。所以,需要執(zhí)行 seal(value) 而不是 seal.fulfill(value)。這是因?yàn)?Guarantees 只處理 fulfill,而不處理其他情況,
Guarantee<T>
從 PromiseKit 5 開始,我們提供了 Guarantee 類來作為 Promise 的補(bǔ)充。這樣做為補(bǔ)充 swift 強(qiáng)大的錯(cuò)誤處理系統(tǒng)。
Guarantees 表示沒有失敗的情況,所以他們沒有 rejected 狀態(tài)。一個(gè)很好的例子就是 after :
firstly {
after(seconds: 0.1)
}.done {
// there is no way to add a `catch` because after cannot fail.
}
如果你不終止常規(guī)的 Promise 調(diào)用鏈,swift 將會(huì)發(fā)出警告。你應(yīng)該通過 catch 或者 retrun 來消除警告。(在下面的例子中,你只能在 promise 的末尾使用 catch。)
讓你的程序中,盡可能的使用 Guarantees,在需要處理錯(cuò)誤情況時(shí),寫錯(cuò)誤處理程序,在不需要時(shí),不寫錯(cuò)誤處理程序。
通常情況下,Guarantee 和 Promise 可以互換使用。為了確保這一點(diǎn),我們已經(jīng)做了足夠多的測(cè)試,如果你發(fā)現(xiàn)還有問題,請(qǐng)和我們聯(lián)系。
創(chuàng)建 guarantees 的語(yǔ)法和 promises 相比,更加的簡(jiǎn)單。
func fetch() -> Promise<String> {
return Guarantee { seal in
fetch { result in
seal(result)
}
}
}
這還可以簡(jiǎn)化為:
func fetch() -> Promise<String> {
return Guarantee(resolver: fetch)
}
map, compactMap, etc.
then 提供了上一個(gè) promise 的結(jié)果,需要返回另一個(gè) promise。
map 提供了上一個(gè) promise 的結(jié)果,需要返回一個(gè)類或者結(jié)果類型。
compactMap 提供了上一個(gè) promise 的結(jié)果,需要返回一個(gè)可選類型。如果返回 nil,調(diào)用鏈就會(huì)拋出 PMKError.compactMap 的錯(cuò)誤而失敗。
說明:在 PromiseKit 4 之前,
then處理了所有的情況,這很糟糕。我們希望在新的 Swift 版本中能有所好轉(zhuǎn)。但是,很明顯,糟糕情況依然存在。事實(shí)上,我們作為框架的作者,非常希望能在命名上消除 API 的歧義。所以我們將then的三種含義拆分為then,map和done。在實(shí)踐中,我們發(fā)現(xiàn)這些新功能更加的好用,所以我們有添加了compactMap(參考了Optional.compactMap方法)。
我們可以使用 compactMap 更加方便的創(chuàng)建 promise 鏈。例如:
firstly {
URLSession.shared.dataTask(.promise, with: rq)
}.compactMap {
try JSONSerialization.jsonObject($0.data) as? [String]
}.done { arrayOfStrings in
//…
}.catch { error in
// Foundation.JSONError if JSON was badly formed
// PMKError.compactMap if JSON was of different type
}
提醒:我們還提供了很多你可能會(huì)在數(shù)組中用到的方法,例如:
map,thenMap,compatMapValues,firstValue,等等。
get
get 就行 done 一樣,不過他可以返回結(jié)果流。
firstly {
foo()
}.get { foo in
//…
}.done { foo in
// same foo!
}
tap
tap 用于測(cè)試??雌饋砗?get 一樣,但是它提供了 promise 的 Result<T>,所以你可以在某處查看調(diào)用鏈的值,而不產(chǎn)生任何副作用:
firstly {
foo()
}.tap {
print($0)
}.done {
//…
}.catch {
//…
}
補(bǔ)充
firstly
我們已經(jīng)使用了 firstly 幾次,但是它究竟是什么?事實(shí)上,它就是一個(gè)語(yǔ)法糖。你不一定需要它,但是它可以讓你的調(diào)用鏈的可讀性更高。可以將下面的代碼進(jìn)行替換:
firstly {
login()
}.then { creds in
//…
}
替換為:
login().then { creds in
//…
}
理解這個(gè)很關(guān)鍵:login() 返回一個(gè) Promise,并且所有的 Promise 有一個(gè) then 方法。firstly 返回一個(gè) Promise,并且 then 也返回一個(gè) Promise。不用太關(guān)心其中的細(xì)節(jié)。先了解這種模式,等你想進(jìn)一步了解的時(shí)候,再去學(xué)習(xí)底層的架構(gòu)。
when 的變體
在 PromiseKit 當(dāng)中,when 是一個(gè)非常有用的方法,所以,我們提供了幾種變體。
- 默認(rèn)的 when,也就是你通常用的是
when(fulfilled:)。這種情況是,組成 when 的所有 promise 中,只要有一個(gè)失敗,則 when 就會(huì)失敗, 接著調(diào)用鏈就會(huì)變成 rejects 狀態(tài)。有一點(diǎn)非常重要:構(gòu)成 when 的所有 Promise 都會(huì)繼續(xù)執(zhí)行。Promise 沒有可以控制他們包含的任務(wù)。Promise 只是將任務(wù)進(jìn)行了一層包裝。 -
when(resolved:)這個(gè)變體是:組成 when 的所有 Promise 中即使有一個(gè)或多個(gè) Promise 失敗了,它依然提供了一個(gè) Result<T> 的結(jié)果。因?yàn)?,這個(gè)變體要求所有構(gòu)成 when 的 Promise 有相同的泛型。這個(gè)限制如何解除,可以查看指南的進(jìn)階部分。 -
race變體可以讓幾個(gè) Promise 進(jìn)行競(jìng)爭(zhēng)。哪個(gè)先結(jié)束,結(jié)果就是哪個(gè)。更詳細(xì)的用法可以查看指南的進(jìn)階部分。
Swift 閉包推斷
對(duì)于一行的閉包,swift 會(huì)自行推斷返回值和返回值的類型。下面的兩種形式效果一樣:
foo.then {
bar($0)
}
// is the same as:
foo.then { baz -> Promise<String> in
return bar(baz)
}
在文檔中,為了更加的簡(jiǎn)潔,我們經(jīng)常省略掉 return。
但是,這種簡(jiǎn)寫既是福,又是禍。你會(huì)發(fā)現(xiàn) Swift 編譯器經(jīng)常不能準(zhǔn)確的推斷出返回值類型。你可以查看 Troubleshooting Guide 來獲取更多的幫助。
在 PromiseKit 5 中,可以通過使用
done來避免這個(gè)痛點(diǎn)。
進(jìn)一步學(xué)習(xí)
在使用的過程中,上面的內(nèi)容涵蓋了 90% 的情況。強(qiáng)烈的建議你再閱讀一下 API Reference。這里有很多對(duì)你來說非常有用的小功能,其中的文檔也將會(huì)對(duì)上述內(nèi)容從源碼層面進(jìn)行更詳細(xì)的說明。