獨立部落格 ZYF.IM
在 GitHub Trending 中總是看到 mxcl/PromiseKit 它是主要解決的是 “回撥地獄” 的問題,決定嘗試用一下。
環境:Swift 4.2、PromiseKit 6
then and done
下面是一個典型的 promise 鏈式(chain)呼叫:
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}
複製程式碼
如果這段程式碼使用完成回撥(completion handler
)實現,他將是:
login { creds, error in
if let creds = creds {
fetch(avatar: creds.user) { image, error in
if let image = image {
self.imageView = image
}
}
}
}
複製程式碼
then
是完成回撥的另一種方式,但是它更豐富。在處級階段的理解,它更具有可讀性。上面的 promise chain 更容易閱讀和理解:一個非同步操作接著另一個,一行接一行。它與程式程式碼非常接近,因為我們很容易得到 Swift 的當前狀態。
done
與 then
基本是一樣的,但是它將不再返回 promise。它是典型的在末尾 “success” 部分的 chain。在上面的例子 done
中,我們接收到了最終的圖片並使用它設定了 UI。
讓我們對比一下兩個 login
的方法簽名:
// Promise:
func login() -> Promise<Creds>
// Compared with:
func login(completion: (Creds?, Error?) -> Void)
// 可選型,兩者都是可選
複製程式碼
區別在於 promise,方法返回 promises 而不是的接受和執行回撥。每一個處理器(handler)都會返回一個 promise。Promise 物件們定義 then
方法,該方法在繼續鏈式呼叫之前等待 promise 的完成。chains 在程式上解決,一次一個 promise。
Promise 代表未來非同步方法的輸入值。它有一個表示它包裝的物件型別的型別。例如,在上面的例子裡,login
的返回 Promise 值代表一個 Creds 的一個例項。
可以注意到這與 completion pattern 的不同,promises chain 似乎忽略錯誤。並不是這樣,實際上:promise chain 使錯誤處理更容易訪問(accessible),並使錯誤更難被忽略。
catch
有了 promises,錯誤在 promise chain 上級聯(cascade along),確保你的應用的健壯(robust)和清晰的程式碼。
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch {
// 整個 chain 上的錯誤都到了這裡
}
複製程式碼
如果你忘記了 catch 這個 chain,Swift 會發出警告
每個 promise 都是一個表示單個(individual)非同步任務的物件。如果任務失敗,它的 promise 將成為 rejected
。產生 rejected
promises 將跳過後面所有的 then
,而是將執行 catch
。(嚴格上說是執行後續所有的 catch
處理)
這與 completion handler 對比:
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
和合並錯誤對處理有所保證,但是 promise chain 更具有可讀性。
ensure
firstly {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
return login()
}.then {
fetch(avatar: $0.user)
}.done {
self.imageView = $0
}.ensure {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
// ...
}
複製程式碼
無論在 chain 哪裡結束,成功或者失敗,ensure
終將被執行。也可以使用 finally
來完成相同的事情,區別是沒有返回值。
spinner(visible: true)
firstly {
foo()
}.done {
// ...
}.catch {
// ...
}.finally {
self.spinner(visible: false)
}
複製程式碼
when
多個非同步操作同時處理時可能又難又慢。例如當 操作1
和 操作2
都完成時再返回結果:
// 序列操作
operation1 { result1 in
operation2 { result2 in
finish(result1, result2)
}
}
複製程式碼
// 並行操作
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)
}
複製程式碼
使人 Promises 將變得容易很多:
firstly {
when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
// ...
}
複製程式碼
when
等待所有的完成再返回 promises 結果。
PromiseKit 擴充套件
PromiseKit 提過了一些 Apple API 的擴充套件,例如:
firstly {
CLLocationManager.promise()
}.then { location in
CLGeocoder.reverseGeocode(location)
}.done { placemarks in
self.placemark.text = "\(placemarks.first)"
}
複製程式碼
同時需要指定 subspaces:
pod "PromiseKit"
pod "PromiseKit/CoreLocation"
pod "PromiseKit/MapKit"
複製程式碼
更多的擴充套件可以查詢 PromiseKit organization,甚至擴充套件了 Alamofire 這樣的公共庫。
製作 Promises
有時你的 chains 仍然需要以自己開始,或許你使用的三方庫沒有提供 promises 或者自己寫了非同步系統,沒關係,他們非常容易新增 promises。如果你檢視了 PromiseKit 的標準擴充套件,可以看到使用了下面相同的描述:
已有程式碼:
func fetch(completion: (String?, Error?) -> Void)
複製程式碼
轉換:
func fetch() -> Promise<String> {
return Promise { fetch(completion: $0.resolve) }
}
複製程式碼
更具有可讀性的:
func fetch() -> Promise<String> {
return Promise { seal in
fetch { result, error in
seal.resolve(result, error)
}
}
}
複製程式碼
Promise 初始化程式提供的 seal
物件定義了很多處理 garden-variety
完成回撥的方法。
PromiseKit 設定嘗試以
Promise(fetch)
進行處理,但是完成通過編譯器的消歧義。
Guarantee
從 PromiseKit 5 開始,提供了 Guarantee 以做補充,目的是完善 Swift 強的的異常處理。
Guarantee
永遠不會失敗,所以不能被 rejected
。
firstly {
after(seconds: 0.1)
}.done {
// 這裡不要加 catch
}
複製程式碼
Guarantee
的語法相較更簡單:
func fetch() -> Promise<String> {
return Guarantee { seal in
fetch { result in
seal(result)
}
}
}
// 減少為
func fetch() -> Promise<String> {
return Guarantee(resolver: fetch)
}
複製程式碼
map compactMap 等
then
要求返回另一個 promisemap
要求返回一個 object 或 value 型別compactMap
要求返回一個 可選型,如過返回nil
,chain 將失敗並報錯PMKError.compactMap
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
}
複製程式碼
除此之外還有:thenMap
compactMapValues
firstValue
etc
get
get
會得到 done
中相同值。
firstly {
foo()
}.get { foo in
// ...
}.done { foo in
// same foo!
}
複製程式碼
tap
為 debug 提供 tap
,與 get
類似但是可以得到 Result<T>
這樣就可以檢查 chain 上的值:
firstly {
foo()
}.tap {
print($0)
}.done {
// ...
}.catch {
// ...
}
複製程式碼
補充
firstly
上面例子中的 firstly
是語法糖,非必須但是可以讓 chains 更有可讀性。
firstly {
login()
}.then { creds in
// ...
}
// 也可以
login().then { creds in
// ...
}
複製程式碼
知識點:login()
返回了一個 Promise
,同時所有的 Promise
有一個 then
方法。firstly
返回一個 Promise
,同樣 then
也返回一個 Promise
。
when 變種
when(fulfilled:)
在所有非同步操作執行完後才執行回撥,一個失敗 chain 將 rejects。It's important to note that all promises in the when continue. Promises have no control over the tasks they represent. Promises are just wrappers around tasks.when(resolved:)
使一個或多個元件承諾失敗也會等待。此變體when
生成的值是Result<T>
的陣列,所有要保證相同的泛型。race
只要有一個非同步操作執行完畢,就立刻執行then
回撥。其它沒有執行完畢的非同步操作仍然會繼續執行,而不是停止。
Swift 閉包介面
Swift 有自動推斷返回值和單行返回。
foo.then {
bar($0)
}
// is the same as:
foo.then { baz -> Promise<String> in
return bar(baz)
}
複製程式碼
這樣有好有壞,具體可以查詢 Troubleshooting
更多閱讀
- 強力建議閱讀 API Reference
- 在 Xcode 使用 optinon-click 閱讀 PromiseKit 程式碼
- 在網上有一些 PMK < 5 的文章,裡面的 API 有些不同要注意
Reference:
-- EOF --