PromiseKit 入門使用

一帆_荔枝發表於2019-03-01

獨立部落格 ZYF.IM

promisekit-getting-started

在 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 的當前狀態。

donethen 基本是一樣的,但是它將不再返回 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 要求返回另一個 promise
  • map 要求返回一個 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 --