一文帶你使用 Swift 實現(xiàn) Promise
前言
我最近在找如何使用 Swift 實現(xiàn) Promise 的資料,因為沒找到好的文章,所以我想自己寫一篇。通過本文,我們將實現(xiàn)自己的 Promise 類型,以便明了其背后的邏輯。
要注意這個實現(xiàn)完全不適合生產(chǎn)環(huán)境。例如,我們的 Promise 沒有提供任何錯誤機制,也沒有覆蓋線程相關的場景。我會在文章的后面提供一些有用的資源以及完整實現(xiàn)的鏈接,以饗愿深入挖掘的讀者。
注:為了讓本教程更有趣一點,我選擇使用 TDD 來進行介紹。我們會先寫測試,然后確保它們一個個通過。
第一個測試
先寫第一個測試:
- test(named: "0. Executor function is called immediately") { assert, done in
- var string: String = ""
- _ = Promise { string = "foo" }
- assert(string == "foo")
- done()
- }
通過此測試,我們想實現(xiàn):傳遞一個函數(shù)給 Promise 的初始化函數(shù),并立即調用此函數(shù)。
注:我們沒有使用任何測試框架,僅僅使用一個自定義的test方法,它在 Playground 中模擬斷言(gist[1])。
當我們運行 Playground,編譯器會報錯:
- error: Promise.playground:41:9: error: use of unresolved identifier 'Promise'
- _ = Promise { string = "foo" }
- ^~~~~~~
合理,我們需要定義 Promise 類。
- class Promise {
- }
再運行,錯誤變?yōu)椋?/p>
- error: Promise.playground:44:17: error: argument passed to call that takes no arguments
- _ = Promise { string = "foo" }
- ^~~~~~~~~~~~~~~~~~
我們必須定義一個初始化函數(shù),它接受一個閉包作為參數(shù)。而且這個閉包應該被立即調用。
- class Promise {
- init(executor: () -> Void) {
- executor()
- }
- }
由此,我們通過第一個測試。目前我們還沒有寫出什么值得夸耀的東西,但耐心一點,我們的實現(xiàn)將在下一節(jié)繼續(xù)增長。
- • Test 0. Executor function is called immediately passed
我們先將此測試注釋掉,因為將來的 Promise 實現(xiàn)會變得有些不同。
最低限度
第二個測試如下:
- 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()
- }
- }
這個測試挺簡單,但我們添加了一些新內(nèi)容到 Promise 類。我們創(chuàng)建的這個 promise 有一個 resolution handler(即閉包的resolve參數(shù)),之后立即調用它(傳遞一個 value)。然后,我們使用 promise 的 then 方法來訪問 value 并用斷言確保其值。
在開始實現(xiàn)之前,我們需要引入另外一個不太一樣的測試。
- 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 是一個狀態(tài)機。當它被創(chuàng)建時,promise 處于pending狀態(tài)。一旦resolve方法被調用(與一個 value),我們的 promise 將轉到resolved狀態(tài),并存儲這個 value。
then方法可在任意時刻被調用,而不管 promise 的內(nèi)部狀態(tài)(即不管 promise 是否已有一個 value)。當這個 promise 處于pending狀態(tài)時,我們調用then,value 將不可用,因此,我們需要存儲此回調。之后一旦 promise 變成resolved,我們就能使用 resolved value 來觸發(fā)同樣的回調。
現(xiàn)在我們對要實現(xiàn)的東西有了更好的理解,那就先以修復編譯器的報錯開始。
- error: Promise.playground:54:19: error: cannot specialize non-generic type 'Promise'
- let promise = Promise<String> { resolve in
- ^ ~~~~~~~~
我們必須給Promise類型添加泛型。誠然,一個 promise 是這樣的東西:它關聯(lián)著一個預定義的類型,并能在被解決時,將一個此類型的 value 保留住。
- class Promise<Value> {
- init(executor: () -> Void) {
- executor()
- }
- }
現(xiàn)在錯誤為:
- 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函數(shù)傳遞給初始化函數(shù)(即 executor)。
- class Promise<Value> {
- init(executor: (_ resolve: (Value) -> Void) -> Void) {
- executor()
- }
- }
注意這個 resolve 參數(shù)是一個函數(shù),它消耗一個 value:(Value) -> Void。一旦 value 被確定,這個函數(shù)將被外部世界調用。
編譯器依然不開心,因為我們需要提供一個resolve函數(shù)給executor。讓我們創(chuàng)建一個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
- }
- }
我們將在稍后實現(xiàn)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
- }
- }
現(xiàn)在編譯器開心了,讓我們回到開始的地方。
我們之前說過一個Promise就是一個狀態(tài)機,它有一個pending狀態(tài)和一個resolved狀態(tài)。我們可以使用 enum 來定義它們。
- enum State<T> {
- case pending
- case resolved(T)
- }
Swift 的美妙讓我們可以直接存儲 promise 的 value 在 enum 中。
現(xiàn)在我們需要在Promise的實現(xiàn)中定義一個狀態(tài),其默認值為.pending。我們還需要一個私有函數(shù),它能在當前還處于.pending狀態(tài)時更新狀態(tài)。
- 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:)函數(shù)先檢查了當前處于.pending狀態(tài)。如果 promise 已經(jīng)處于.resolved狀態(tài),那它就不能再變成其他狀態(tài)了。
現(xiàn)在是時候在必要時更新 promise 的狀態(tài),即,當resolve函數(shù)被外部世界傳遞 value 調用時。
- private func resolve(_ value: Value) -> Void {
- updateState(to: .resolved(value))
- }
快好了,只缺少 then 方法還未實現(xiàn)。我們說過必須存儲回調,并在 promise 被解決時調用回調。這就來實現(xiàn)之。
- 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狀態(tài)時保留回調。同時我們創(chuàng)建一個方法triggerCallbackIfResolved,它先檢查狀態(tài)是否為.resolved,然后傳遞拆包的 value 給回調。這個方法在兩個地方被調用。一個是then方法中,如果 promise 已經(jīng)在調用then時被解決。另一個在updateState方法中,因為那是 promise 更新其內(nèi)部狀態(tài)從.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實現(xiàn)。先來看看測試。
- 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 中,當?shù)谝粋€then被調用時,promise 還處于.pending狀態(tài)。如我們之前所見,我們存儲了第一次then的回調。但當我們第二次調用then時,promise 還是沒有被解決,依然處于.pending狀態(tài),于是,我們將回調擦除換成了新的。只有第二個回調會在將來被執(zhí)行,第一個被忘記了。這使得測試雖然通過,但只有一個斷言而不是兩個。
解決辦法也很簡單,就是存儲一個回調的數(shù)組,并在promise被解決時觸發(fā)它們。
讓我們更新一下。
- 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)
恭喜!我們已經(jīng)創(chuàng)建了自己的Promise類。你已經(jīng)可以使用它來抽象異步邏輯,但它還有限制。
注:如果從全局來看,我們知道then可以被重命名為observe。它的目的是消費 promise 被解決后的 value,但它不返回什么。這意味著我們暫時沒法串聯(lián)多個 promise。
串聯(lián)多個 Promise
如果我們不能串聯(lián)多個 promise,那我們的Promise實現(xiàn)就不算完整。
先來看看測試,它將幫助我們實現(xiàn)這個特性。
- 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創(chuàng)建了一個有新 value 的新Promise并返回了它。第二個then(我們前一節(jié)定義的,被稱為observe)被串聯(lián)在后面,它訪問新的 value(其將是"foofoo")。
我們很快在終端里看到錯誤。
- error: Promise.playground:143:10: error: value of tuple type '()' has no member 'then'
- .then { value in
- ^
我們必須創(chuàng)建一個then的重載,它接受一個能返回 promise 的函數(shù)。為了能夠串聯(lián)調用then,這個方法必須也返回一個promise。這個then的原型如下。
- func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> {
- // to implement
- }
注:細心的讀者可能已經(jīng)發(fā)現(xiàn),我們在給Promise實現(xiàn)flatMap。就如給Optional和Array定義flatMap一樣,我們也可以給Promise定義它。
困難來了。讓我們一步步看看這個“flatMap”的then要怎么實現(xiàn)。
- 我們需要返回一個Promise
- 誰給我們這樣一個 promise?onResolved 方法
- 但onResolved 需要一個類型為Value的 value 為參數(shù)。我們該怎樣得到這個 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的閉包所限制。我們不能將其作為函數(shù)的返回值。
我們要使用的技巧是創(chuàng)建一個包裝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)
串聯(lián)多個 value
如果你能給某個類型實現(xiàn)flatMap,那你就能利用flatMap為其實現(xiàn)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。
編譯器報錯說我們必須實現(xiàn)此方法。
- error: Promise.playground:174:26: error: declared closure result 'String' is incompatible with contextual type 'Void'
- .then { value -> String in
- ^~~~~~
- Void
這個方法很接近flatMap,唯一的不同是其參數(shù)onResolved函數(shù)返回一個NewValue而不是Promise
- // map
- func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> {
- // to implement
- }
之前我們說可以利用flatMap實現(xiàn)map。在我們的情況里,我們看到我們需要返回一個Promise
- // 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方法的實現(xiàn),能被使用或串聯(lián)。
- // 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))
- }
- }
- }
使用示例
實現(xiàn)告一段落。我們的Promise類已足夠完整來展示我們能夠用它做什么。
假設我們的app有一些用戶,結構如下:
- struct User {
- let id: Int
- let name: String
- }
假設我們還有兩個方法,一個獲取用戶id列表,另一個使用id獲取某個用戶。而且假設我們想顯示第一個用戶的名字。
通過我們的實現(xiàn),我們可以這樣做,使用之前定義的這三個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)
- }
代碼變得十分易讀、簡潔,而且沒有嵌套。
結論本文結束,希望你喜歡它。
參考資料
[1]gist:
https://gist.github.com/felginep/039ca3b21e4f0cabb1c06126d9164680
[2]Promises in Swift by Khanlou:
http://khanlou.com/2016/08/promises-in-swift/
[3]JavaScript Promises … In Wicked Detail:
https://www.mattgreer.org/articles/promises-in-wicked-detail/
[4]PromiseKit 6 Release Details:
https://promisekit.org/news/2018/02/PromiseKit-6.0-Released/
[5]TDD Implementation of Promises in JavaScript:
https://www.youtube.com/watch?v=C3kUMPtt4hY
[6]Implementing Promises in Swift:
https://felginep.github.io/2019-01-06/implementing-promises-in-swift
本文轉載自微信公眾號「Swift社區(qū)」