Promise 的幾種通用模式

gaoxinqd 7年前發布 | 13K 次閱讀 iOS開發 移動開發

英文原文發布時間較早,故原文代碼中的 Swift 版本較舊,但是作者已將 GitHub 上的 Promise 示例代碼 更新到了最新 Swift 版本,所以譯者在翻譯本文時,將文章里的代碼按照 GitHub 上的示例代碼進行了替換,更新成了最新版本的 Swift 代碼。

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

Promise.all

Promise.all 是其中的典型,它保存所有異步回調的值。這個靜態函數的作用是等待所有的 Promise 執行 fulfill(履行) ,一旦全部執行完畢, Promise.all 會使用所有履行后的值組成的數組對自己執行 fulfill。例如,你可能想在代碼中對數組中的每個元素打點以捕獲某個 API 的完成狀態。使用 map 和 Promise.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.race 是 Promise.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 不會產生任何影響。

有了這個函數,使用 timeout 和 Promise.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 。

基于之前編寫的 delay 和 recover 函數構建了重試的函數。

在上面的這些例子中,輕量且可組合的部分組合在一起,就得到了簡單優雅的解決方案。所有的這些行為都是建立在 Promise 核心代碼所提供的簡單的 .then 和 catch 函數上的。通過格式化完成閉包的樣式,可以解決諸如超時、恢復、重試以及其他可以通過簡單可重用的方式解決的問題。這些例子仍然需要一些測試和驗證,我會在未來一段時間內慢慢地添加到 GitHub 倉庫 中。

 

 

來自:http://swift.gg/2017/05/04/common-patterns-with-promises/

 

 本文由用戶 gaoxinqd 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!