使用 Swift 實現 Promise
使用 Swift 實現 Promise
本文翻譯自:https://felginep.github.io/2019-01-06/implementing-promises-in-swift
原作者:Pierre Felgines
譯者:@nixzhu
我最近在找如何使用 Swift 實現 Promise 的資料,因為沒找到好的文章,所以我想自己寫一篇。通過本文,我們將實現自己的 Promise 型別,以便明瞭其背後的邏輯。
要注意這個實現完全不適合生產環境。例如,我們的 Promise 沒有提供任何錯誤機制,也沒有覆蓋執行緒相關的場景。我會在文章的後面提供一些有用的資源以及完整實現的連結,以饗願深入挖掘的讀者。
注:為了讓本教程更有趣一點,我選擇使用 TDD 來進行介紹。我們會先寫測試,然後確保它們一個個通過。
第一個測試
先寫第一個測試:
test(named: "0. Executor function is called immediately") { assert, done in var string: String = "" _ = Promise { string = "foo" } assert(string == "foo") done() }
通過此測試,我們想實現:傳遞一個函式給 Promise 的初始化函式,並立即呼叫此函式。
注:我們沒有使用任何測試框架,僅僅使用一個自定義的test
方法,它在 Playground 中模擬斷言(gist
)。
當我們執行 Playground,編譯器會報錯:
error: Promise.playground:41:9: error: use of unresolved identifier 'Promise' _ = Promise { string = "foo" } ^~~~~~~
合理,我們需要定義 Promise 類。
class Promise { }
再執行,錯誤變為:
error: Promise.playground:44:17: error: argument passed to call that takes no arguments _ = Promise { string = "foo" } ^~~~~~~~~~~~~~~~~~
我們必須定義一個初始化函式,它接受一個閉包作為引數。而且這個閉包應該被立即呼叫。
class Promise { init(executor: () -> Void) { executor() } }
由此,我們通過第一個測試。目前我們還沒有寫出什麼值得誇耀的東西,但耐心一點,我們的實現將在下一節繼續增長。
• Test 0. Executor function is called immediately passed
我們先將此測試註釋掉,因為將來的 Promise 實現會變得有些不同。
最低限度
第二個測試如下:
test(named: "1.1 Resolution handler is called when promise is resolved sync") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in resolve(string) } promise.then { (value: String) in assert(string == value) done() } }
這個測試挺簡單,但我們添加了一些新內容到 Promise 類。我們建立的這個 promise 有一個 resolution handler(即閉包的resolve
引數),之後立即呼叫它(傳遞一個 value)。然後,我們使用 promise 的then
方法來訪問 value 並用斷言確保其值。
在開始實現之前,我們需要引入另外一個不太一樣的測試。
test(named: "1.2 Resolution handler is called when promise is resolved async") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in after(0.1) { resolve(string) } } promise.then { (value: String) in assert(string == value) done() } }
不同於測試 1.1,這裡的resove
方法被延遲呼叫。這意味著,在then
裡,value 不會立馬可用(因為 0.1 秒的延遲,呼叫then
時,resolve
還未被呼叫)。
我們開始理解這裡的“問題”。我們必須處理非同步。
我們的 promise 是一個狀態機。當它被建立時,promise 處於pending
狀態。一旦resolve
方法被呼叫(與一個 value),我們的 promise 將轉到resolved
狀態,並存儲這個 value。
then
方法可在任意時刻被呼叫,而不管 promise 的內部狀態(即不管 promise 是否已有一個 value)。當這個 promise 處於pending
狀態時,我們呼叫then
,value 將不可用,因此,我們需要儲存此回撥。之後一旦 promise 變成resolved
,我們就能使用 resolved value 來觸發同樣的回撥。
現在我們對要實現的東西有了更好的理解,那就先以修復編譯器的報錯開始。
error: Promise.playground:54:19: error: cannot specialize non-generic type 'Promise' let promise = Promise<String> { resolve in ^~~~~~~~~
我們必須給Promise
型別新增泛型。誠然,一個 promise 是這樣的東西:它關聯著一個預定義的型別,並能在被解決時,將一個此型別的 value 保留住。
class Promise<Value> { init(executor: () -> Void) { executor() } }
現在錯誤為:
error: Promise.playground:54:37: error: contextual closure type '() -> Void' expects 0 arguments, but 1 was used in closure body let promise = Promise<String> { resolve in ^
我們必須提供一個resolve
函式傳遞給初始化函式(即 executor)。
class Promise<Value> { init(executor: (_ resolve: (Value) -> Void) -> Void) { executor() } }
注意這個 resolve 引數是一個函式,它消耗一個 value:(Value) -> Void
。一旦 value 被確定,這個函式將被外部世界呼叫。
編譯器依然不開心,因為我們需要提供一個resolve
函式給executor
。讓我們建立一個private
的吧。
class Promise<Value> { init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } private func resolve(_ value: Value) -> Void { // To implement // This will be called by the outside world when a value is determined } }
我們將在稍後實現resolve
,當所有錯誤都被解決時。
下一個錯誤很簡單,方法then
還未定義。
error: Promise.playground:61:5: error: value of type 'Promise<String>' has no member 'then' promise.then { (value: String) in ^~~~~~~ ~~~~
讓我們修復之。
class Promise<Value> { init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } func then(onResolved: @escaping (Value) -> Void) { // To implement } private func resolve(_ value: Value) -> Void { // To implement } }
現在編譯器開心了,讓我們回到開始的地方。
我們之前說過一個Promise
就是一個狀態機,它有一個pending
狀態和一個resolved
狀態。我們可以使用 enum 來定義它們。
enum State<T> { case pending case resolved(T) }
Swift 的美妙讓我們可以直接儲存 promise 的 value 在 enum 中。
現在我們需要在Promise
的實現中定義一個狀態,其預設值為.pending
。我們還需要一個私有函式,它能在當前還處於.pending
狀態時更新狀態。
class Promise<Value> { enum State<T> { case pending case resolved(T) } private var state: State<Value> = .pending init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } func then(onResolved: @escaping (Value) -> Void) { // To implement } private func resolve(_ value: Value) -> Void { // To implement } private func updateState(to newState: State<Value>) { guard case .pending = state else { return } state = newState } }
注意updateState(to:)
函式先檢查了當前處於.pending
狀態。如果 promise 已經處於.resolved
狀態,那它就不能再變成其他狀態了。
現在是時候在必要時更新 promise 的狀態,即,當resolve
函式被外部世界傳遞 value 呼叫時。
private func resolve(_ value: Value) -> Void { updateState(to: .resolved(value)) }
快好了,只缺少then
方法還未實現。我們說過必須儲存回撥,並在 promise 被解決時呼叫回撥。這就來實現之。
class Promise<Value> { enum State<T> { case pending case resolved(T) } private var state: State<Value> = .pending // we store the callback as an instance variable private var callback: ((Value) -> Void)? init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } func then(onResolved: @escaping (Value) -> Void) { // store the callback in all cases callback = onResolved // and trigger it if needed triggerCallbackIfResolved() } private func resolve(_ value: Value) -> Void { updateState(to: .resolved(value)) } private func updateState(to newState: State<Value>) { guard case .pending = state else { return } state = newState triggerCallbackIfResolved() } private func triggerCallbackIfResolved() { // the callback can be triggered only if we have a value, // meaning the promise is resolved guard case let .resolved(value) = state else { return } callback?(value) callback = nil } }
我們定義了一個例項變數callback
,以在 promise 處於.pending
狀態時保留回撥。同時我們建立一個方法triggerCallbackIfResolved
,它先檢查狀態是否為.resolved
,然後傳遞拆包的 value 給回撥。這個方法在兩個地方被呼叫。一個是then
方法中,如果 promise 已經在呼叫then
時被解決。另一個在updateState
方法中,因為那是 promise 更新其內部狀態從.pending
到.resolved
的地方。
有了這些修改,我們的測試就成功通過了。
• Test 1.1 Resolution handler is called when promise is resolved sync passed (1 assertions) • Test 1.2 Resolution handler is called when promise is resolved async passed (1 assertions)
我們走對了路,但我們還需要做出一點改變,以得到一個真正的Promise
實現。先來看看測試。
test(named: "2.1 Promise supports many resolution handlers sync") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in resolve(string) } promise.then { value in assert(string == value) } promise.then { value in assert(string == value) done() } }
test(named: "2.2 Promise supports many resolution handlers async") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in after(0.1) { resolve(string) } } promise.then { value in assert(string == value) } promise.then { value in assert(string == value) done() } }
這回我們對每個 promise 都呼叫了兩次then
。
先看看測試輸出。
• Test 2.1 Promise supports many resolution handlers sync passed (2 assertions) • Test 2.2 Promise supports many resolution handlers async passed (1 assertions)
雖然測試通過了,但你可能也注意問題。測試 2.2 只有一個斷言,但應該是兩個。
如果我們思考一下,這其實符合邏輯。誠然,在非同步的測試 2.2 中,當第一個then
被呼叫時,promise 還處於.pending
狀態。如我們之前所見,我們儲存了第一次then
的回撥。但當我們第二次呼叫then
時,promise 還是沒有被解決,依然處於.pending
狀態,於是,我們將回調擦除換成了新的。只有第二個回撥會在將來被執行,第一個被忘記了。這使得測試雖然通過,但只有一個斷言而不是兩個。
解決辦法也很簡單,就是儲存一個回撥的陣列,並在promise被解決時觸發它們。
讓我們更新一下。
class Promise<Value> { enum State<T> { case pending case resolved(T) } private var state: State<Value> = .pending // We now store an array instead of a single function private var callbacks: [(Value) -> Void] = [] init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } func then(onResolved: @escaping (Value) -> Void) { callbacks.append(onResolved) triggerCallbacksIfResolved() } private func resolve(_ value: Value) -> Void { updateState(to: .resolved(value)) } private func updateState(to newState: State<Value>) { guard case .pending = state else { return } state = newState triggerCallbacksIfResolved() } private func triggerCallbacksIfResolved() { guard case let .resolved(value) = state else { return } // We trigger all the callbacks callbacks.forEach { callback in callback(value) } callbacks.removeAll() } }
測試通過,而且都有兩個斷言。
• Test 2.1 Promise supports many resolution handlers sync passed (2 assertions) • Test 2.2 Promise supports many resolution handlers async passed (2 assertions)
恭喜!我們已經建立了自己的Promise
類。你已經可以使用它來抽象非同步邏輯,但它還有限制。
注:如果從全域性來看,我們知道then
可以被重新命名為observe
。它的目的是消費 promise 被解決後的 value,但它不返回什麼。這意味著我們暫時沒法串聯多個 promise。
串聯多個 Promise
如果我們不能串聯多個 promise,那我們的Promise
實現就不算完整。
先來看看測試,它將幫助我們實現這個特性。
test(named: "3. Resolution handlers can be chained") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in after(0.1) { resolve(string) } } promise .then { value in return Promise<String> { resolve in after(0.1) { resolve(value + value) } } } .then { value in // the "observe" previously defined assert(string + string == value) done() } }
如測試所見,第一個then
建立了一個有新 value 的新Promise
並返回了它。第二個then
(我們前一節定義的,被稱為observe
)被串聯在後面,它訪問新的 value(其將是"foofoo"
)。
我們很快在終端裡看到錯誤。
error: Promise.playground:143:10: error: value of tuple type '()' has no member 'then' .then { value in ^
我們必須建立一個then
的過載,它接受一個能返回 promise 的函式。為了能夠串聯呼叫then
,這個方法必須也返回一個promise。這個then
的原型如下。
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { // to implement }
注:細心的讀者可能已經發現,我們在給Promise
實現flatMap
。就如給Optional
和Array
定義flatMap
一樣,我們也可以給Promise
定義它。
困難來了。讓我們一步步看看這個“flatMap”的then
要怎麼實現。
-
我們需要返回一個
Promise<NewValue>
-
誰給我們這樣一個 promise?
onResolved
方法 -
但
onResolved
需要一個型別為Value
的 value 為引數。我們該怎樣得到這個 value?我們可以使用之前定義的then
(或者說 “observe
”) 來在其可用時訪問它
如果寫成程式碼,大概如下:
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { then { value in // the "observe" one let promise = onResolved(value) // `promise` is a Promise<NewValue> // problem: how do we return `promise` to the outside ?? } return // ??!! }
就快好了。但我們還有個小問題需要修復:這個promise
變數被傳遞給then
的閉包所限制。我們不能將其作為函式的返回值。
我們要使用的技巧是建立一個包裝Promise<NewValue>
,它將執行我們目前所寫的程式碼,然後在promise
變數解決時被同時解決。換句話說,當onResolved
方法提供的 promise 被解決並從外部得到一個值,那包裝的 promise 也就被解決並得到同樣的值。
可能文字有些抽象,但如果我們寫成程式碼,將看得更清楚:
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { // We have to return a promise, so let's return a new one return Promise<NewValue> { resolve in // this is called immediately as seen in test 0. then { value in // the "observe" one let promise = onResolved(value) // `promise` is a Promise<NewValue> // `promise` has the same type of the Promise wrapper // we can make the wrapper resolves when the `promise` resolves // and gets a value promise.then { value in resolve(value) } } } }
如果我們整理一下程式碼,我們將有這樣兩個方法:
// observe func then(onResolved: @escaping (Value) -> Void) { callbacks.append(onResolved) triggerCallbacksIfResolved() } // flatMap func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { return Promise<NewValue> { resolve in then { value in onResolved(value).then(onResolved: resolve) } } }
最後,測試通過。
• Test 3. Resolution handlers can be chained passed (1 assertions)
串聯多個 value
如果你能給某個型別實現flatMap
,那你就能利用flatMap
為其實現map
。對於我們的Promise
來說,map
該是什麼樣子?
我們將使用如下測試:
test(named: "4. Chaining works with non promise return values") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in after(0.1) { resolve(string) } } promise .then { value -> String in return value + value } .then { value in // the "observe" then assert(string + string == value) done() } }
注意第一個then
沒有再返回一個Promise
,而是將其接收的值做了一個變換。這個新的then
就對應於我們想新增的map
。
編譯器報錯說我們必須實現此方法。
error: Promise.playground:174:26: error: declared closure result 'String' is incompatible with contextual type 'Void' .then { value -> String in ^~~~~~ Void
這個方法很接近flatMap
,唯一的不同是其引數onResolved
函式返回一個NewValue
而不是Promise<NewValue>
。
// map func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> { // to implement }
之前我們說可以利用flatMap
實現map
。在我們的情況裡,我們看到我們需要返回一個Promise<NewValue>
。如果我們使用這個“flatMap”的then
,並建立一個promise,再以對映後的 value 來直接解決,我們就搞定了。讓我來證明之。
// map func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> { return then { value in // the "flatMap" defined before // must return a Promise<NewValue> here // this promise directly resolves with the mapped value return Promise<NewValue> { resolve in let newValue = onResolved(value) resolve(newValue) } } }
再一次,測試通過。
• Test 4. Chaining works with non promise return values passed (1 assertions)
如果我們移除註釋,再看看我們做出了什麼。我們有三個then
方法的實現,能被使用或串聯。
// observe func then(onResolved: @escaping (Value) -> Void) { callbacks.append(onResolved) triggerCallbacksIfResolved() } // flatMap func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { return Promise<NewValue> { resolve in then { value in onResolved(value).then(onResolved: resolve) } } } // map func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> { return then { value in return Promise<NewValue> { resolve in resolve(onResolved(value)) } } }
使用示例
實現告一段落。我們的Promise
類已足夠完整來展示我們能夠用它做什麼。
假設我們的app有一些使用者,結構如下:
struct User { let id: Int let name: String }
假設我們還有兩個方法,一個獲取使用者id列表,另一個使用id獲取某個使用者。而且假設我們想顯示第一個使用者的名字。
通過我們的實現,我們可以這樣做,使用之前定義的這三個then
。
func fetchIds() -> Promise<[Int]> { ... } func fetchUser(id: Int) -> Promise<User> { ... } fetchIds() .then { ids in // flatMap return fetchUser(id: ids[0]) } .then { user in // map return user.name } .then { name in // observe print(name) }
程式碼變得十分易讀、簡潔,而且沒有巢狀。
結論
本文結束,希望你喜歡它。
你可以在這個gist 找到完整程式碼。如果你想進一步理解,下面是一些我使用的資源。
- Promises in Swift by Khanlou
- JavaScript Promises … In Wicked Detail
- PromiseKit 6 Release Details
- TDD Implementation of Promises in JavaScript
歡迎轉載,但請一定註明出處:https://github.com/nixzhu/dev-blog !