在異步編程中,除了竟態(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ū)中著名的 q、bluebird 都是用來(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。