ReactiveCocoa vs RxSwift
原文:Rui Peres on April 26, 2016

函數(shù)式響應(yīng)型編程(Functional Reactive Programming)是一種變得越來越流行的編程范式,尤其是在 Swift 開發(fā)者之中。它將復(fù)雜的異步過程,變得容易編寫和理解。
在這篇文章里,你將可以對(duì)比函數(shù)式響應(yīng)型編程中最流行的兩個(gè)框架:RxSwift 和 ReactiveCocoa。
下面來簡單的了解一下,什么是函數(shù)式響應(yīng)型編程?然后詳細(xì)的比較一下這兩個(gè)框架,了解了這些之后,你就可以選擇一個(gè)適合你的框架來使用。
那么開始吧!
什么是函數(shù)式響應(yīng)型編程?
如果你已經(jīng)熟悉了函數(shù)式響應(yīng)型編程的概念,可以跳過這一章節(jié),直接閱讀下一章節(jié) ReactiveCocoa vs RxSwift。
甚至在 Swift 出現(xiàn)之前,函數(shù)式響應(yīng)型編程(FRP)在近些年來歡受迎程度就大幅增長,與面向?qū)ο缶幊绦纬甚r明對(duì)比。從 Haskell 到 Go,再到 Javascript,都有 FRP 的實(shí)現(xiàn)方式。為什么呢?FRP 到底有什么特異功能?
最重要的問題是,你如何將這種編程范式應(yīng)用到 Swift 上呢?
函數(shù)式響應(yīng)型編程是由 Conal Elliott 創(chuàng)建的一種編程范式。他給了一個(gè)嚴(yán)謹(jǐn)詳細(xì)的語義定義,你可以從這里去進(jìn)一步了解。簡單定義的話,F(xiàn)RP 是由兩種概念組合而成的:
- 響應(yīng)型編程(Reactive Programming),它關(guān)注的是異步數(shù)據(jù)流,你需要監(jiān)聽并根據(jù)其中的數(shù)據(jù)做出響應(yīng)。想要了解更多,看看這篇不錯(cuò)的介紹。
- 函數(shù)式編程(Functional Programming),他的函數(shù)定義具有數(shù)學(xué)風(fēng)格,計(jì)算過程中盡量避免使用變量和狀態(tài)值,代碼更加靈活無副作用。想要了解更多,請(qǐng)看我們的另一篇 "Swift functional programming tutorial"。
André Staltz 在他的文章 "Why I cannot say FRP but I just did" 中,闡述了標(biāo)準(zhǔn)的 FRP 范式和它的可實(shí)現(xiàn)方式之間的差別。
一個(gè)簡單的例子
?想要理解這個(gè)概念最簡單的方式,就是通過例子來說明。想象有一款應(yīng)用,需要關(guān)注用戶的位置變化,并且在發(fā)現(xiàn)他的位置靠近一個(gè)咖啡店的時(shí)候提示他。
通過 FRP 的方式需要這樣實(shí)現(xiàn):
- 需要?jiǎng)?chuàng)建一個(gè)對(duì)象,它發(fā)出你需要響應(yīng)的位置變化事件的數(shù)據(jù)流。
- 然后通過篩選這些位置信息,把那些靠近咖啡店的位置信息顯示出來。
在 ReactiveCocoa 中,代碼大概長這樣:
locationProducer // 1
.filter(ifLocationNearCoffeeShops) // 2
.startWithNext {[weak self] location in // 3
self?.alertUser(location)
}
說明一下這部分代碼:
-
locationProducer在位置信息每次發(fā)生變化的時(shí)候,都會(huì)拋出一個(gè)事件(event)。ReactiveCocoa 把它稱作signal,RxSwift 中稱作sequence。 - 然后利用函數(shù)式編程(Functional Programming)技術(shù)去處理這些變化事件的數(shù)據(jù)。
filter函數(shù)的用法和數(shù)組(array)中的相同,將數(shù)據(jù)流中的每個(gè)值作為參數(shù)傳給ifLocationNearCoffeeShops處理,如果返回true,這個(gè)事件會(huì)繼續(xù)傳遞到下一步。 - 最后,
startWithNext就是一個(gè)訂閱方法,當(dāng)有(過濾過的)事件傳遞到這里的時(shí)候,你傳入的閉包表達(dá)式將會(huì)執(zhí)行,并將那些數(shù)據(jù)作為參數(shù)提供給你。
上面的代碼看起來很像是將數(shù)組中的數(shù)據(jù)進(jìn)行轉(zhuǎn)換。但是這個(gè)更高級(jí)...它是異步的;那個(gè)過濾方法和閉包表達(dá)式只有在位置變化事件發(fā)生的時(shí)候才會(huì)被執(zhí)行。
語法看起來也許會(huì)怪一些,不過希望你可以明白這段代碼所表達(dá)的意思。它既體現(xiàn)了函數(shù)式編程的精髓,又十分貼切 values over time(直譯:時(shí)間軸上的數(shù)據(jù))的概念:這也就是它的定義。你需要關(guān)心的是數(shù)據(jù)來了,而不是產(chǎn)生這些數(shù)據(jù)的細(xì)節(jié)。
如果你想了解更多的 ReactiveCocoa 語法,去看看我寫的一些例子吧:Github 地址。
事件轉(zhuǎn)換
在上面的例子中,僅僅是關(guān)注了位置變化的數(shù)據(jù)流,除了過濾一些靠近咖啡店的位置,并沒有對(duì)這些事件做更多的處理。
而 FRP 范式的另一個(gè)基本要素,就是能夠?qū)⑦@些事件數(shù)據(jù)進(jìn)行組合、轉(zhuǎn)換,使其變得更有意義一些。怎么做呢?你可以利用(但不限于)一些高階函數(shù)。
你可以在我們的教程 Swift functional programming tutorial 中,找到一些常見的函數(shù):map、filter、reduce、combine、zip。
我們來優(yōu)化一下這段代碼,過濾掉那些重復(fù)的位置信息,并且將傳過來的位置數(shù)據(jù)(CLLocation)轉(zhuǎn)換成一段用戶可識(shí)別的文本信息。
locationProducer
.skipRepeats() // 1
.filter(ifLocationNearCoffeeShops)
.map(toHumanReadableLocation) // 2
.startWithNext {[weak self] readableLocation in
self?.alertUser(readableLocation)
}
我們來看看新加的兩行代碼:
- 首先在
locationProducer發(fā)出數(shù)據(jù)流之后,加一步skipRepeats操作,這個(gè)操作并不是array所具有的;它是 ReactiveCocoa 所特有的。這個(gè)方法的意圖很明顯:過濾掉那些相等的數(shù)據(jù)事件(這些事件的數(shù)據(jù)需要具有可比性)。 - 在
filter方法執(zhí)行過后,map函數(shù)的作用就是將一種事件數(shù)據(jù)轉(zhuǎn)換成另一種,比如把CLLocation類型轉(zhuǎn)換成String類型。
現(xiàn)在,你已經(jīng)體會(huì)到了 FRP 的一些非凡之處了吧:
- 它使用簡單,卻功能強(qiáng)大。
- 它使代碼更加容易被理解。
- 復(fù)雜的數(shù)據(jù)流,變得容易管理和描述。

ReactiveCocoa 和 RxSwift 簡述
現(xiàn)在你已經(jīng)對(duì)什么是 FRP 有了一個(gè)比較好的認(rèn)識(shí),并且知道了它如何幫助你簡單的管理復(fù)雜的異步數(shù)據(jù)流。接下來讓我們來看看這兩個(gè)流行的 FRP 框架:ReactiveCocoa 和 RxSwift,然后你可以挑一個(gè)適合你的來使用。
詳細(xì)分析之前,我們先簡單了解一下這兩個(gè)框架的發(fā)展史。
ReactiveCocoa
ReactiveCocoa 是在 Github 上發(fā)布的。當(dāng)時(shí)開發(fā)者們?cè)?Github Mac 客戶端上工作,他們發(fā)現(xiàn)很難管理他們應(yīng)用的數(shù)據(jù)流。后來他們從微軟的 ReactiveExtensions(一個(gè) C# 的 FRP 實(shí)現(xiàn))那里得到靈感,創(chuàng)建了他們的 Objective-C 實(shí)現(xiàn)。
當(dāng)他們正準(zhǔn)備發(fā)布 Objective-C 實(shí)現(xiàn)的 3.0 版本的時(shí)候,Swift 發(fā)布了。他們意識(shí)到 Swift 的函數(shù)式風(fēng)格更適合 ReactiveCocoa,所以他們馬上著手于 Swift 的實(shí)現(xiàn),成為了 3.0 版本。3.0 版本更具函數(shù)式風(fēng)格,利用到了柯里化(currying)和 pipe-forward 運(yùn)算符技術(shù)。
Swift 2.0 引入了面向協(xié)議編程(protocol-oriented programming 概念,這也導(dǎo)致了 ReactiveCocoa API 另一個(gè)具有重大意義的變化,4.0 版本減少使用 pipe-forward 運(yùn)算符,而開始運(yùn)用協(xié)議拓展。
寫這篇文章時(shí),ReactiveCocoa 已經(jīng)是在 Github 上已經(jīng)收獲了超過 13000 顆星的非常流行的庫了。
RxSwift
微軟的 ReactiveExtensions 啟發(fā)許多其他的框架,將 FRP 的概念融入了 JavaScript,Java,Scala,還有許多其他的編程語言。最終形成了 ReactiveX,一個(gè)為 FRP 實(shí)現(xiàn)方式創(chuàng)建通用 API 的組織;這允許許多框架作者可以協(xié)同工作。也正因?yàn)檫@樣,一個(gè)熟悉 RxScal(Scala 的實(shí)現(xiàn))的開發(fā)者會(huì)發(fā)現(xiàn)把它轉(zhuǎn)換成 Java 類似的實(shí)現(xiàn)(RxJava)將會(huì)很容易。
RxSwift 是相對(duì)較新加入 ReactiveX 的,而且不如 ReactiveCocoa 更加流行(寫這段話時(shí),它在 Github 上大概有 4000 顆星)。不過事實(shí)上,RxSwift 作為 ReactiveX 的一部分,毫無疑問將會(huì)很流行并長久發(fā)展下去。
值得一提的是,RxSwift 與 ReactiveCocoa 都有一個(gè)共同的原型:ReactiveExtensions。
RxSwift 與 ReactiveCocoa 對(duì)比
是時(shí)候該了解一下細(xì)節(jié)了。RxSwift 與 ReactiveCocoa 對(duì)于 FRP 的支持體現(xiàn)了許多不同的方面,讓我們來看看他們其中重要的幾部分。
熱信號(hào)、冷信號(hào)
想象一下,你需要發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求,并且解析它的響應(yīng)數(shù)據(jù),然后展示給用戶:
let requestFlow = networkRequest.flatMap(parseResponse)
requestFlow.startWithNext {[weak self] result in
self?.showResult(result)
}
只有當(dāng)你訂閱一個(gè) signal(也就是進(jìn)行 startWithNext 操作)的時(shí)候,網(wǎng)絡(luò)請(qǐng)求才會(huì)被創(chuàng)建并發(fā)起。這種 signal 被稱作是冷(cold)的,因?yàn)檎缒闼碌降?,直到你訂閱它們之前,它是處于“凍結(jié)”狀態(tài)的。
另一種則稱為熱(hot) 信號(hào),當(dāng)訂閱一個(gè)熱信號(hào)時(shí),它就已經(jīng)開始了,所以你可以觀察到第三或者第四次事件,或者更多。比較典型的例子就是敲擊鍵盤產(chǎn)生的事件流。對(duì)于“開始”敲擊鍵盤,并沒有什么意義,就像創(chuàng)建一個(gè)服務(wù)器請(qǐng)求。
總結(jié)一下:
- 一個(gè)冷信號(hào)是指,當(dāng)你想要訂閱他的時(shí)候,需要執(zhí)行開始任務(wù),每個(gè)新的訂閱者都需要執(zhí)行開始任務(wù),訂閱
requestFlow三次也就意味著相對(duì)應(yīng)的要?jiǎng)?chuàng)建三個(gè)網(wǎng)路請(qǐng)求。 - 一個(gè)熱信號(hào)創(chuàng)建時(shí)就已經(jīng)可以發(fā)送事件了,訂閱者不需要去開啟它。通常 UI 交互是屬于熱信號(hào)。
ReactiveCocoa 針對(duì)熱、冷信號(hào)分別提供了這兩種類型:Signal<T,E> 與 SignalProducer<T,E>。而 RxSwift 提供了一種同時(shí)支持冷、熱信號(hào)的類型:Observable<T>。
區(qū)分熱信號(hào)、冷信號(hào)兩種不同的類型真的有必要么?
我個(gè)人認(rèn)為,知道一個(gè)信號(hào)的含義是很重要的,因?yàn)樗玫拿枋隽巳绾卧谝粋€(gè)特定的上下文中運(yùn)用它。當(dāng)處理一個(gè)復(fù)雜的系統(tǒng)時(shí),這些將會(huì)有很大不同。
且不說有沒有這兩種類型的支持,僅僅了解熱信號(hào)、冷信號(hào)這些概念就非常重要。
正如 André Staltz 所說的:
如果你忽略了它,那么它一定會(huì)回來給你狠狠的一擊。別說我沒告訴過你。
假設(shè)你正在處理一個(gè)熱信號(hào),然后由于某種原因它變成了冷信號(hào),這個(gè)時(shí)候你將會(huì)對(duì)每個(gè)訂閱者進(jìn)行副作用編程。這將會(huì)給你的應(yīng)用帶來很大影響。舉個(gè)通用的例子,假設(shè)在你的應(yīng)用中,有三個(gè)或四個(gè)實(shí)體需要監(jiān)聽同一種網(wǎng)絡(luò)請(qǐng)求,而對(duì)于每個(gè)新的訂閱,都會(huì)發(fā)起一個(gè)新的網(wǎng)絡(luò)請(qǐng)求。
ReactiveCocoa 加一分!
錯(cuò)誤處理
討論錯(cuò)誤處理之前,我們先概括一下 RxSwift 和 ReactiveCocoa 分發(fā)的事件的一些性質(zhì)。這兩個(gè)框架中,主要有三種事件類型:
-
Next<T>:每當(dāng)一個(gè)新的值(T類型)被傳到事件流中時(shí),這種事件就會(huì)被觸發(fā)。在上面跟蹤定位的那個(gè)例子中,T指的就是CLLocation。 -
Compleled:表示事件流的終止。收到這個(gè)事件之后,將不會(huì)在發(fā)送Next<T>和Error<E>。 -
Error<E>:表示一個(gè)錯(cuò)誤。在服務(wù)器請(qǐng)求的例子中,當(dāng)你收到一個(gè)服務(wù)器錯(cuò)誤時(shí),這個(gè)事件將會(huì)被發(fā)送。E是遵循了ErrorType協(xié)議的錯(cuò)誤類型。收到這個(gè)事件之后,將不會(huì)在發(fā)送Next<T>和Compleled。
你應(yīng)該已經(jīng)注意到上一章節(jié)提到 ReactiveCocoa 的 Singal<T, E> 和 SignalProducer<T, E> 有兩個(gè)參數(shù)類型,而 RxSwift 的 Observable<T> 只有一個(gè)。前者的第二個(gè)類型(E)是遵循了 ErrorType 協(xié)議的子類型。在 RxSwift 中這個(gè)類型被刪除了,取而代之的是一個(gè)需要內(nèi)部處理的 ErrorType 協(xié)議類型。
所以這些是什么意思呢?
實(shí)際上,這意味著在 RxSwift 中,錯(cuò)誤可以從許多不同的地方拋出來。
create { observer in
observer.onError(NSError.init(domain: "NetworkServer", code: 1, userInfo: nil))
}
以上創(chuàng)建了一個(gè)信號(hào)(或者,RxSwift 中稱之為觀察序列(observable sequence)),然后立刻拋出了一個(gè)錯(cuò)誤。
這是另一個(gè):
create { observer in
observer.onError(MyDomainSpecificError.NetworkServer)
}
一個(gè) Observable 強(qiáng)制錯(cuò)誤必須只能是遵從 ErrorType 類型的,你可以發(fā)送任何你需要的錯(cuò)誤類型。但是可能有些不方便,例如下面的例子:
enum MyDomanSpecificError: ErrorType {
case NetworkServer
case Parser
case Persistence
}
func handleError(error: MyDomanSpecificError) {
// Show alert with the error
}
observable.subscribeError {[weak self] error in
self?.handleError(error)
}
這段代碼無效,因?yàn)楹瘮?shù) handleError 希望得到的是 MyDomainSpecificError 類型,而不是 ErrorType 類型。所以你必須做兩件事:
- 試著將
error轉(zhuǎn)換成MyDomainSpecificError類型。 - 處理當(dāng)
error不能轉(zhuǎn)成MyDomainSpecificError的情況。
第一點(diǎn)很容易利用 as? 來解決,但是第二點(diǎn)不太容易確定如何處理。一個(gè)可能的解決方案就是提供一個(gè) Unknown 類型:
enum MyDomanSpecificError: ErrorType {
case NetworkServer
case Parser
case Persistence
case Unknown
}
observable.subscribeError {[weak self] error in
self?.handleError(error as? MyDomanSpecificError ?? .Unknown)
}
在 ReactiveCocoa 中,當(dāng)你創(chuàng)建一個(gè) Signal<T,E> 或者 SignalProducer<T,E> 時(shí),就相當(dāng)于解決了上述的第一點(diǎn),如果你想要傳遞其他類型,編譯器會(huì)給警告。最后:ReactiveCocoa 中,編譯器不允許你傳一個(gè)不同于你之前指定好的錯(cuò)誤類型。
ReactiveCocoa 再加一分!
UI 綁定
在 iOS 的標(biāo)準(zhǔn) API 中,比如 UIKit,并不是使用的 FRP 的語言語法。所以為了使用 RxSwift 和 ReactiveCocoa,你必須橋接這些 API,例如將點(diǎn)擊事件(利用 target-action 編碼形式)轉(zhuǎn)換成 signal 或 observable。
正如你所想象的,這需要許多工作,所以這兩個(gè)庫都額外提供了許多橋和綁定。
ReactiveCocoa 帶來了許多從 Objective-C 時(shí)期完成的工作。你會(huì)發(fā)現(xiàn)有一部分已經(jīng)完成了,并且已經(jīng)橋接到了 Swift 上。這些只包括 UI 綁定,其他操作還沒有沒翻譯成 Swift。所以,顯得有些奇怪。你正在使用一個(gè)不是 Swift API 中的類型(比如 RACSignal),然后又強(qiáng)制用戶將 Objective-C 類型轉(zhuǎn)為 Swift(例如使用 toSignalProducer() 方法)。
不止這些,我覺得我看源碼的時(shí)間比看文檔的時(shí)間都長,這顯然跟不上時(shí)代。注意這點(diǎn)是很重要的,雖然從解釋理論和思路上來講,文檔寫的很好,但從使用角度上來說還遠(yuǎn)遠(yuǎn)不夠。
正是由于這點(diǎn),你會(huì)發(fā)現(xiàn)有許多的 ReactiveCocoa 教程。
與此不同的是,RxSwift 提供的綁定很容易使用!你不僅能看到巨長的目錄,還能找到巨多的例子,還有一份更加完善的文檔。對(duì)于一些人來說,這已經(jīng)足夠讓你去選擇 RxSwift,而不選 ReactiveCocoa 了。
RxSwift 加一分!
社團(tuán)
ReactiveCocoa 比起 RxSwift 很久之前就已經(jīng)出現(xiàn)了。有許多人還在繼續(xù)維護(hù),在網(wǎng)上也有很多教程,而且通過在 StackOverflow 的 ReactiveCocoa 標(biāo)簽可以找到很好的資源。
ReactiveCocoa 有一個(gè) Slack 的群,不過很小只有 209 個(gè)人,所以有許多人提的問題(包括我提的和其他人提的)都沒有回答。由于時(shí)間緊急,我被迫私信了 ReactiveCocoa 的核心成員,所以我猜測(cè)其他人也可能和我的情況的一樣。不過,你很有可能在網(wǎng)上找一個(gè)教程來解釋你的問題。
RxSwift 是新人,不過現(xiàn)在卻很有一枝獨(dú)秀的感覺。他也有一個(gè) Slack 的群,而且很大已經(jīng)有 961 個(gè)成員了,群內(nèi)討論熱烈。你也可以比較容易在這里找到回答你問題的人。
總之吧,現(xiàn)在兩個(gè)框架的社團(tuán)支持以各自不同的強(qiáng)大方式支持著,所以這個(gè)小節(jié),打成平手。
你該如何選擇?
正如 Ash Furrow 在 “ReactiveCocoa vs RxSwift” 中所說的:
“聽我的,如果你是一個(gè)新手,選哪個(gè)真的沒有關(guān)系。是的,雖然他們有些技術(shù)上的區(qū)別,不過對(duì)于新手沒太大意義。試著用其中一個(gè),然后再試試另一個(gè)??纯茨膫€(gè)更適合你,然后在想想為什么你會(huì)選擇它?!?/p>
我的意見也大概如此。只有當(dāng)你有足夠的體驗(yàn)之后,才能領(lǐng)會(huì)他們之中細(xì)微的差別。
不過,如果你現(xiàn)在正處于一個(gè)需要選擇其中一個(gè),但是又沒時(shí)間去使用它們的情況,可以看看我的意見:
選擇 ReactiveCocoa,如果:
- 你想要更好的描述你的系統(tǒng)。用不同的類型來區(qū)分熱信號(hào)和冷信號(hào),同時(shí)通過類型化參數(shù)處理錯(cuò)誤,對(duì)于你的系統(tǒng)會(huì)有很好的效果。
- 想要一個(gè)大規(guī)模的測(cè)試框架,被許多人使用在不同的項(xiàng)目中。
選擇 RxSwift,如果:
- UI 綁定對(duì)于你的系統(tǒng)很重要
- 你是一個(gè) FRP 新手,希望得到一些手把手的教學(xué)。
- 你已經(jīng)了解了 RxJS 或者 RxJava。因?yàn)樗鼈兒?RxSwift 都是屬于 ReactiveX 組織的,如果你了解其中之一,其他的也只是語法上的不同而已。
何去何從?
無論你選擇了 ReactiveCocoa 或者選擇 RxSwift,你都不會(huì)后悔的。它們都是很強(qiáng)大的框架,并且可以幫助你很好的描述你的系統(tǒng)。
需要注意的很重要的一點(diǎn)是,一旦你選擇 RxSwift 或 ReactiveCocoa 其中之一,想要切換到另一方也只是幾個(gè)小時(shí)的問題。就我以鍛煉的目的,從 ReactiveCocoa 轉(zhuǎn)到 RxSwift 的經(jīng)驗(yàn)來說,大部分麻煩的問題也就是錯(cuò)誤處理了??偨Y(jié)來說,最大的思想轉(zhuǎn)變就是完全使用 FRP,而不是僅僅實(shí)現(xiàn)其中一個(gè)部分。
以下的鏈接可以幫助你融入函數(shù)式響應(yīng)性編程、RxSwift 和 ReactiveCocoa 中:
- Conal Elliott 的博客。
- Conal Elliott 在 Stackoverflow 對(duì)于 “What is (functional) reactive programming?” 強(qiáng)大的回答。
- André Staltz,必須讀的文章 “Why I cannot say FRP but I just did”。
- RxSwift 的 Github 地址。
- ReactiveCocoa 的 Github 地址。
- Rex 的 Github 地址。
- iOS 開發(fā)者最終的 FRP 寶庫。這里你能夠找到包括 RxSwift 和 ReactiveCocoa 兩者的資源。
- 我們 Marin Todorov 的 RxSwift 探索。