Swift 中實(shí)現(xiàn) Promise 模式

在異步編程中,除了竟態(tài)處理、資源利用以外,另外一個(gè)難點(diǎn)就是流程管理。在擁有匿名函數(shù)、閉包這些特性的編程語(yǔ)言中,我們通??梢允褂没卣{(diào)函數(shù)來(lái)做一個(gè)異步任務(wù)完成或失敗時(shí)的處理。但當(dāng)我們的業(yè)務(wù)邏輯逐漸復(fù)雜時(shí),就會(huì)產(chǎn)生回調(diào)嵌套,整個(gè)事件流將十分混亂。相信大家對(duì) Node.js 的回調(diào)陷阱一定有所耳聞了。于是各種各樣事件流處理的庫(kù)就產(chǎn)生了,比如 NPM.js 社區(qū)中著名的 qbluebird 都是用來(lái)解決回調(diào)陷阱的,它們所采用的模式就是我們所說(shuō)的 Promise,也是我們今天要談的模式。當(dāng)然異步流程處理的方式遠(yuǎn)不止 Promise 這一種,在支持生成器語(yǔ)義的語(yǔ)言里我們還能用 yield + generator 來(lái)實(shí)現(xiàn)協(xié)程,co就是這種思想。但是 Swift 并不支持,所以我們就不討論了。

Getting Started

相信大家在日常開(kāi)發(fā)中對(duì)下面這種模式已經(jīng)不陌生了:

dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) { 
    // Do something in background.
    dispatch_async(dispatch_get_main_queue()) {
        // Update the UI.
    }
}

類似用法的還有諸如權(quán)限認(rèn)證、NSURLSession,都有這樣的回調(diào),如果邏輯稍微復(fù)雜一些就會(huì)出現(xiàn)回調(diào)陷阱。下面我們應(yīng)用 Promise 來(lái)將它們平坦化,通過(guò)連接就可以實(shí)現(xiàn)鏈?zhǔn)降氖录憫?yīng)。

我們先看看用了 Promise 后是一番怎樣的景象:

doSomething()
    .then( doAnotherThing )
    .then( doSomethingElse )
    .success( someHandler )

是不是瞬間清晰了很多?我們來(lái)分析一下如何實(shí)現(xiàn)。
首先,doSomething() 會(huì)返回一個(gè) Promise 對(duì)象,Promise 對(duì)象在構(gòu)造函數(shù)中接受一個(gè)函數(shù)作為參數(shù),這個(gè)函數(shù)就是用來(lái)啟動(dòng)異步任務(wù)的。而這個(gè)函數(shù)又有兩個(gè)參數(shù),分別是成功時(shí)的回調(diào)和失敗時(shí)的回調(diào)。

我們下面就來(lái)實(shí)現(xiàn)這個(gè) Promise 類:

class Promise<T> {
    typealias ResolveCallback = (T) -> Void
    typealias RejectCallback = (ErrorType) -> Void
    typealias AsyncTask = (ResolveCallback, RejectCallback) -> Void
    
    let task: AsyncTask
    
    var resolveCallback: ResolveCallback?
    var rejectCallback: RejectCallback?
    
    init(_ task: AsyncTask) {
        self.task = task
    }
}

通過(guò)泛型表示異步任務(wù)最終得到的結(jié)果的類型,對(duì)于失敗的情況,傳遞一個(gè)實(shí)現(xiàn) ErrorType 協(xié)議的對(duì)象作為錯(cuò)誤的原因。

下面我們看如何響應(yīng)結(jié)果并啟動(dòng)任務(wù):

    ...

    func success(callback: ResolveCallback) {
        self.resolveCallback = callback
        self.task({ self.resolve($0) }, { self.reject($0) })
    }
    
    func failed(callback: RejectCallback) {
        self.rejectCallback = callback
    }

    ...

我這里采用了冷啟動(dòng)的方式,也就是說(shuō)僅當(dāng)有響應(yīng)函數(shù)被掛載時(shí)才啟動(dòng)異步任務(wù),如果你想在 Promise 一被創(chuàng)建時(shí)就啟動(dòng)異步任務(wù)(也就是熱啟動(dòng)),就需要用一個(gè)屬性來(lái)存放任務(wù)的結(jié)果,以免響應(yīng)函數(shù)還沒(méi)被掛載,這個(gè)異步任務(wù)就完成了,這樣這個(gè)結(jié)果就丟失了。

異步任務(wù)啟動(dòng)的方式是將 resolve 、 reject 作為參數(shù)傳給異步任務(wù)啟動(dòng)函數(shù),當(dāng)異步任務(wù)自身的回調(diào)調(diào)用時(shí),這個(gè) Promise 對(duì)象就能做出響應(yīng)處理,將這個(gè)事件傳遞給它的響應(yīng)函數(shù)。

當(dāng)然,resolve 、 reject函數(shù)也很簡(jiǎn)單,就是執(zhí)行回調(diào):

    private func resolve(result: T) {
        self.resolveCallback?(result)
    }
    
    private func reject(error: ErrorType) {
        self.rejectCallback?(error)
    }

鏈?zhǔn)秸{(diào)用

到現(xiàn)在我們并沒(méi)有實(shí)現(xiàn) Promise 的精髓,還不能鏈?zhǔn)秸{(diào)用。所謂的鏈?zhǔn)秸{(diào)用就是當(dāng)一個(gè) Promise 完成時(shí),立即用一個(gè)變換函數(shù)將結(jié)果傳給下一個(gè) Promise 去執(zhí)行,以此類推,這樣我們構(gòu)造一串操作之后再一并啟動(dòng),實(shí)現(xiàn)上面我說(shuō)到的冷啟動(dòng)。

那么這個(gè)核心函數(shù)就是 then,這個(gè)函數(shù)接受一個(gè)變換函數(shù)作為參數(shù),這個(gè)變換函數(shù)接受 Promise 結(jié)果然后構(gòu)造出一個(gè)新的 Promise 對(duì)象,然后返回。由于 then 返回的也是 Promise 因此我們可以用鏈?zhǔn)秸Z(yǔ)法不斷連接多個(gè)操作,十分方便。

那么連接后的啟動(dòng)順序回事怎樣的呢?舉個(gè)例子:

promise1.then(genPromise2).then(genPromise3).success(...)

那么最終得到的 Promise 對(duì)象就是這樣的:

promise3( promise2( promise1 ) )

最后一個(gè) then 連接的 Promise 對(duì)象包裹了上一個(gè)對(duì)象,以此類推,那么在啟動(dòng)時(shí),當(dāng)然也是最后一個(gè)對(duì)象被啟動(dòng),只不過(guò)由于這個(gè) Promise 對(duì)象是被封裝過(guò)的,所以它會(huì)先觸發(fā)上一個(gè)對(duì)象,待拿到結(jié)果后再傳遞給自己執(zhí)行,仍然以此類推,就能保證執(zhí)行順序是正確的。

說(shuō)的很抽象,直接看代碼吧:

    func then<U>(f: (T) -> Promise<U>) -> Promise<U> {
        return Promise<U> { (resolve, reject) in
            self.task(
                { (result) in
                    let wrapped = f(result)
                    wrapped.success { resolve($0) }
                },
                { (error) in
                    reject(error)
            })
        }
    }

可以看到我們?cè)趦蓚€(gè) Promise 對(duì)象之間又封裝了一個(gè) Promise 對(duì)象,作為協(xié)調(diào)者,它會(huì)先執(zhí)行之前的異步任務(wù),然后再傳給下一個(gè)任務(wù),那么這個(gè)函數(shù)就會(huì)返回封裝后的 Promise 對(duì)象。

下面是完整的代碼:

class Promise<T> {
    typealias ResolveCallback = (T) -> Void
    typealias RejectCallback = (ErrorType) -> Void
    typealias AsyncTask = (ResolveCallback, RejectCallback) -> Void
    
    let task: AsyncTask
    
    var resolveCallback: ResolveCallback?
    var rejectCallback: RejectCallback?
    
    init(_ task: AsyncTask) {
        self.task = task
    }
    
    private func resolve(result: T) {
        self.resolveCallback?(result)
    }
    
    private func reject(error: ErrorType) {
        self.rejectCallback?(error)
    }
    
    func success(callback: ResolveCallback) {
        self.resolveCallback = callback
        self.task({ self.resolve($0) }, { self.reject($0) })
    }
    
    func failed(callback: RejectCallback) {
        self.rejectCallback = callback
    }
    
    func then<U>(f: (T) -> Promise<U>) -> Promise<U> {
        return Promise<U> { (resolve, reject) in
            self.task(
                { (result) in
                    let wrapped = f(result)
                    wrapped.success { resolve($0) }
                },
                { (error) in
                    reject(error)
            })
        }
    }
}

使用示例

下面我們?cè)囋囋趺词褂眠@個(gè) Promise 對(duì)象,我首先定義三個(gè)操作:

func delay(secs: UInt64) -> Promise<Void> {
    return Promise<Void> { (resolve, _) in
        let time = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC * secs))
        dispatch_after(time, dispatch_get_main_queue()) {
            resolve()
        }
    }
}

func fetch(URL URL: NSURL) -> Promise<NSData?> {
    return Promise<NSData?> { (resolve, reject) in
        let task = NSURLSession.sharedSession().dataTaskWithURL(URL) { (data, response, error) in
            if (error != nil) {
                reject(error!)
            } else {
                resolve(data)
            }
        }
        task.resume()
    }
}

func decodeToString(data: NSData?) -> Promise<String> {
    return Promise<String> { (resolve, _) in
        if (data == nil) {
            resolve("")
        } else {
            resolve(String(data: data!, encoding: NSUTF8StringEncoding) ?? "")
        }
    }
}

這些操作里有異步操作,也有同步操作,但都能適應(yīng) Promise 模式,每個(gè)函數(shù)都返回一個(gè) Promise 對(duì)象。

然后我們構(gòu)造一個(gè)鏈?zhǔn)讲僮鳎?/p>

delay(5)
    .then { () -> Promise<NSData?> in
        fetch(URL: NSURL(string: "https://www.zhihu.com")!)
    }
    .then { (data) -> Promise<String> in
        decodeToString(data)
    }
    .success { (result) in
        print(result)
}

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

注意:我這個(gè)例子是在 Playground 里做的,所以為了讓異步任務(wù)能夠執(zhí)行,我們需要設(shè)置一個(gè)屬性,以免 Playground 在主線程完畢后結(jié)束程序。

結(jié)果符合我們的預(yù)期,在 5 秒的延時(shí)之后,網(wǎng)絡(luò)請(qǐng)求被執(zhí)行,然后轉(zhuǎn)碼,最終打印出來(lái)。

總結(jié)

事實(shí)上,Promise 模式被利用得最好的應(yīng)該是 JavaScript,我們只不過(guò)在其他語(yǔ)言中借鑒這種做法,然后給出響應(yīng)的實(shí)現(xiàn)罷了。當(dāng)然本文只是簡(jiǎn)單剖析 Promise 的內(nèi)部原理,很多細(xì)節(jié)可能沒(méi)有太完善,如果你喜歡這種編程方式,可以嘗試一下成熟的 PromiseKit,它有 Objective-C 和 Swift 的實(shí)現(xiàn)。
當(dāng)然你也可以嘗試一下具有類似思想的反應(yīng)式函數(shù)式編程框架,比如 ReactiveX。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容