主要是爲了總結學習RAC的過程當中,遇到的一些困惑點,一些閱讀的參考資料,文筆也不是很好。建議你們學習RAC參考文章:html
https://github.com/ReactiveCocoa/ReactiveCocoa/tree/master/Documentation以及花瓣工程師的一篇很棒的文章: http://limboy.me/ios/2014/06/06/deep-into-reactivecocoa2.htmlreact
把本身的學習心得寫了一個小demo,放在了github上面,歡迎一塊兒學習交流:https://github.com/lihei12345/RACNetwokDemoios
=====================================================================git
一. ReactiveCocoagithub
monad術語: 「It’s a specific way of chaining operations together. 」 , http://stackoverflow.com/questions/44965/what-is-a-monadobjective-c
1. RACSignal / RACSequence: sql
RACSignal與RACSequence是能夠相互轉換的。RACSignal是push-driven的,RACSequence是pull-driven的。push-driven的意思是在signal建立的時候,signal的values尚未定義,在稍後的某個時間點上,values才能準備好,好比,網絡請求結果或者用戶輸入產生的values。而pull-driven的意思是在sequence建立的時候values就已經被定義了,能夠從sequence中把這些values one-by-one 查詢出來。緩存
1). RACSequence -> RACSignal :網絡
[sequence signal]或者[sequence signalWithScheduler:]app
2). RACSignal -> RACSequence :
[[signal toArray] rac_sequence],注意-toArray方法是阻塞式的,一直到signal completes以後纔會繼續。或者使用[signal sequence]方法,這個方法儘管不會等待signal completes纔會繼續,可是須要signal至少有一個value,因此當signal一個value都沒有的時候,仍然會阻塞。
在實際中,RACSequence使用的並很少,通常就是用來操做Cocoa collections,好比NSArray,NSSet, NSDictionary,NSIndexSet。咱們最感興趣和最經常使用的仍是RACSignal,由於signals表明着將來的values,這個纔是咱們所須要的。
參考: http://rcdp.io/Signal.html
2. RACSubject / RACReplaySubject:
RACSubject用來銜接RAC代碼與非RAC代碼,RACReplaySubject,「A replay subject saves the values it is sent (up to its defined capacity) and resends those to new subscribers. It will also replay an error or completion.」。與RACSubject不一樣的是,RACReplaySubject會緩存它send的值,新的subscribers能夠收到subscribe以前已經產生的值。而且能夠經過設置RACReplaySubject的capacity數量來限制緩存的value的數量,即只緩充最新的幾個值。
3. RACMulticastConnection:
"The main purpose of RACMulticastConnection is to subscribe to a base signal, and then multicast that subscription to any number of other subscribers, without triggering the base signal's side effects multiple times. RACMulticastConnection accomplishes this by sending values to a private RACSubject, which is exposed via the connection's signal property. Subscribers attach to the subject (which doesn't cause any side effects), and the connection forwards all of the base signal's events there."
注意沒有RACMulticastConnection的狀況下,每次subscribe發生時,都會連續觸發到base signal(即源signal)發生side effect,訂閱是一級一級向上傳遞的,直到base signal,能夠參考RACSignal的操做符的實現。
4. RACSignal replay / replayLast / replayLazily:
用於避免subscribe產生屢次side effect,查看源代碼這三個方法的大體調用過程:
生成[RACReplaySubject subject] -> 使用subject調用[RACSignal multicast:] -> 使用multicast:返回的connection來調用 [RACMulticastConnection connect] -> 返回[RACMulticastConnection signal]
與publish的區別,publish在調用[RACSignal multicast:]時使用的是subject是RACSubject,即不會產生value的緩存。
1). replay -> hot signal,會當即subscribe,而且不限制RACReplaySubject的capacity數量
2). replayLast -> hot signal,會當即subscribe,與replay的區別,只是限制RACReplaySubject緩存的capacity爲1,即只保留最新的一個value。
3). replayLazily -> cold signal,"replayLazily does not subscribe to the signal immediately – it lazily waits until there is a 「real」 subscriber. But replay subscribes immediately.",不會當即subscribe,只有真實的subscriber訂閱的時候纔會subscribe,即調用subscribeNext:等的時候。同時,與replayLast不一樣,這裏不會限制RACReplaySubject的capacity,即會保留全部的value。
參考: http://spin.atomicobject.com/2014/06/29/replay-replaylast-replaylazily/
5. RACCommand
「A command, represented by the RACCommand class, creates and subscribes to a signal in response to some action. This makes it easy to perform side-effect work as the user interacts with the app」 — FrameworkOverview
「A command is a signal triggered in response to some action, typically UI-related」 — header file
RACComand是利用相似side effect的方式來實現的,觸發RACCommand的時候,執行command的execute:方法,內部會調用建立RACCommand時傳入的signalBlock()來得到一個signal對象,而後subscribe這個signal來改變RACCommand的各個狀態。
RACCommand有幾個很重要的屬性: executionSignals/errors/executing。
1) 判斷command是否正在運行狀態:
BOOL commandIsExecuting = [[command.executing first] boolValue]; 或者 訂閱command的 [command.executing subscribeNext:]
2) 建立一個cancelable command:
_twitterLoginCommand = [[RACCommand alloc] initWithSignalBlock:^(id _) {
@strongify(self);
return [[self
twitterSignInSignal]
takeUntil:self.cancelCommand.executionSignals];
}];
RAC(self.authenticatedUser) = [self.twitterLoginCommand.executionSignals switchToLatest];
具體能夠詳細參考下面兩篇文章以及前文中提到的github上面的demo:
(1) http://codeblog.shape.dk/blog/2013/12/05/reactivecocoa-essentials-understanding-and-using-raccommand/,這篇文章中對RACCommand的使用講解很是不錯
(2) dispose RACCommand: https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1326,https://github.com/ReactiveCocoa/ReactiveCocoa/issues/963
6. Subscription & Side Effect
1) 能夠理解 subscription block 就是經過 createSignal : 建立RACDynamicSignal時傳入的block,在RACDynamicSignal中全局變量是didSubscribe block,當咱們調用signal的subscribe:方法的時候,核心操做就是調用didSubscribeBlock。這個block被調用的時候,就是Side Effect發生的時候。
2) From: https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/DesignGuidelines.md#side-effects-occur-for-each-subscription,https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/DesignGuidelines.md#make-the-side-effects-of-a-signal-explicit
RACSequence: side effects occur only once. RACSignal: side effects occur for each subscription.
3) From: http://rcdp.io/Signal.html
signals是經過subscription鏈接在一塊兒的,在subscription block中定義的actions只有在subscription發生的時候纔會發生。具體來講,好比咱們使用createSignal:建立的dynamic signal只有當被訂閱的時候,subscription block纔會被真正觸發,若是咱們在這個block中定義了網絡請求,這個時候網絡請求才會真正地被觸發。注意不管中間通過signal多少變換,source signal的subscription block在最終的signal被subscribe的時候都會被處罰。
4) From: http://spin.atomicobject.com/2015/03/19/reactivecocoa-asynchronous-libraries/
RACSignals can be a great way to manage stateful interactions with iOS framework and libraries. To do this properly, it is very important to understand that with a RACSignal, side effects occur for each subscription:
"Each new subscription to a RACSignal will trigger its side effects. This means that any side effects will happen as many times as subscriptions to the signal itself."
But as mentioned in the linked documentation, there are ways to suppress that behavior. For example, a signal can be multicasted.It's also recommended that you make the side effects of a signal explicit when possible: 「 the use of -doNext:, -doError:, and -doCompleted: will make side effects more explicit and self-documenting 「.
To follow this guideline, I try to put all of my calls to external libraries into one of the "do" operators. Sometimes that doesn't make sense though, like in the case of a signal that does nothing besides call an external library. In that case, I'll often use the +defer operator or +createSignal: to wrap the call with side-effects.
5) side effect,指的是RACSignal被subscribe的時候,signal的subscription block就會被執行。
好比,base signal的didSubscribe block內執行一個異步網絡請求等操做,而後在異步網絡請求完成以後,subscriber就會調用相應的步驟,好比[subscriber sendNext:] / [subscriber sendCompletion]等。因此說,每次subscribe產生side effect的話,實際上就會從新建立一個signal(例如發起一個網絡請求)。signal在最終subscribe發生以前,可能會通過一系列的變換,好比,base signal --operator--> A siganl --operator--> B siganl,但不管是A signal仍是B signal被subscribe,base signal的didSubscribe block都會執行,即side effect都會發生。
這裏能夠查看operator的源代碼來查看了解更多細節,每一個operator內部通常來講也是經過[RACSignal createSignal:]以及[self subscribeNext:error:completion:]來生成變換後的新的signal的,這裏createSignal:方法的didSubscribe block也不會當即被調用,只有在這個新的signal被subscribe的時候,纔會執行這個didSubscribe block,而後這個新signal會按subscribe上一級的signal,這樣就實現了signal的鏈式傳遞subscribe,最終subscribe base signal。
這裏還有一點比較容易有誤區的地方,實際上也是我一直比較困惑的地方,就是每次subscribeNext的時候,其實並不會從新生成RACSignal。只是生成一個RACSubscriber保存subcribe時候傳入的block,具體實現來講,好比對於RACDynamicSignal,又會生成一個RACPassRACPassthroughSubscriber用來保持剛生成的RACSubscriber對象以及signal對象(弱引用)。這個一系列的subscribe調用過程,實際上只是生成了一系列的subscriber,並不會對RACSignal的內存有什麼影響,若是最頂部的subscriber在base signal的didSubscribe block中沒有被capture的話,當base signal的didSubscribe block執行完成以後,這一系列的subscriber以及didSubscribe block會當即被釋放。例如 base signal didSubscriber --> subscriber --> (operator)didSubscribe --> subscriber --> didSubscribe...
7. @weakify, @strongify的原理,注意能夠只使用一次@weakify便可,但必須屢次使用@strongify,每一個用到self的block層次都須要使用@strongify來修修飾才能保證不出現retain cycle:http://stackoverflow.com/questions/21716982/explanation-of-how-weakify-and-strongify-work-in-reactivecocoa-libextobjc。這裏有一個須要注意的地方,在block內使用全局變量,也會capture self,也須要使用@strongify來避免內存問題。
8. [RACMulticastConnection autoConnect:] : cold signal. [RACMulticastConnection connect:] : hot signal.
使用[RACMulticastConnection connect]時,signal沒法進行dispose,必須使用[RACMulticastConnection autoConnect]才能夠進行dispose ;因爲全部的replay*默認都是使用connection,因此,全部的replay*沒法進行dispose,side effect中返回的dispose根本不會被調用。
參考: https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1110
9. switchToLatest / flattenMap:
-flattenMap:將stream的每一個value都轉換爲一個新的stream,而後全部這些新生成的streams會被flatten合成爲一個新的stream返回。換句話說,這個過程當中,先執行-map:,再執行-flatten。雖然咱們平時並不會常用這個operator,就像官方文檔說的。這個operator最有趣的地方不是它自身,咱們平常用到這個操做符的場景並很少,而是須要理解它是如何工做的。下面這段翻譯來自於: http://rcdp.io/flattenMap-vs-map-switchToLatest.html,對-flattenMap: 的描述很是清晰。
在FRP理論中,具體一些關於Monad的概念中,-flattenMap: operator 是驅動整個signal鏈式調用的核心機制。-flattenMap: 將每一個來自於source signal的value變換爲另外新的signal,所以會建立一個新的signal-of-signals類型的signal。而後這個signal-of-singles會被flatten,最終返回一個包含全部nested signals中的values的signal。當看到 -map: 方法的實現時,就會發現是經過調用 -flattenMap: 方法來實現的,這個可能會有些困惑。可是當你想起 RACStream 是一個 Monad的時候,這句話就有意義了:全部兩個Monads(RACStream)之間的connections都必須是經過 -flattenMap: 進行表達的。可是注意的是,RACSignal中並非全部操做都是經過-flattenMap:實現的。
雖然-flattenMap:咱們平常場景中使用並很少,主要是用於被其餘操做符使用,可是這篇文章中仍是總結了一些頗有意思的使用場景能夠參考一下(http://spin.atomicobject.com/2014/12/22/reactivecocoa-flattenmap-operator/):
1). Incremental Loading
不少應用爲了加強用戶體驗,一個頁面的數據是被拆分多個請求返回的,等待第一個請求返回一些基本數據以後,纔會發起後續的請求,這樣的小請求返回更快,能夠提早渲染部分UI給用戶,讓用戶不用等待全部的數據返回才能操做。同時也能減輕後臺的開發難度,不用維護很大的接口很複雜的sql。使用-flattenMap:能夠很容易解決這個問題:
2). Mapping Bad Values to Errors
有的狀況下,HTTP請求正常,可是返回的數據是無效的,這個時候,咱們須要本身在subscribeNext:的時候進行判斷,好比下面:
不過,能夠經過-flattenMap:操做符在處理網絡數據的時候,直接把無效的數據直接映射爲error,這樣就不用在寫業務代碼的時候作判斷了,好比:
而後在業務層處理的時候,就不用考慮數據無效的問題了
注意,-switchToLatest:,必須做用於signal-of-signals類型的signal (它全部的value都是signal,好比,調用sendNext:時發送value必須是signal)。這個操做符會自動切換到最後一個signal,並將前一個signal dispose。通常和-map:結合使用較多,比-flattenMap:更爲經常使用。-switchToLatest:的代碼比較簡單,內部也是利用-flattenMap: 和 takeUntil:結合實現的,內部也是調用flattenMap:,可是結合takeUntile:以後,在有新的signal時,就會將以前的signal dispose,這樣就避免了會將多個signal進行合併的問題。
用法參考,http://spin.atomicobject.com/2014/05/21/reactivecocoa-understanding-switchtolatest/:
From: http://rcdp.io/flattenMap-vs-map-switchToLatest.html
-map: + -switchToLatest 與 -flattenMap: 的功能很是接近,主要的一個區別是,前者映射 incoming events 得到的signals不會像後者同樣被合併爲一個signal。反而在-switchToLates,這一些列signals會被按順序處理,一旦收到incoming events映射得到的新signal,當前被subscribes的signal就會被unsubscribes,即被dispose,而後subscribe這個新得到的signal。因此,最終咱們只會看到最新後的這個signal的輸出,以前的signal都會被dispose。
作了一段代碼測試,在 github 上面 https://github.com/lihei12345/RACNetwokDemo:
-flattenMap:
輸出以下:
-map: + -switchToLatest:
輸出以下:
經過上面兩段代碼能夠看出,switchToLatest會把以前的給dispose的。
參考:
1). http://spin.atomicobject.com/2014/05/21/reactivecocoa-understanding-switchtolatest/
2). http://spin.atomicobject.com/2014/12/22/reactivecocoa-flattenmap-operator/
3). http://rcdp.io/flattenMap-vs-map-switchToLatest.html
5). https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/BasicOperators.md#switching
10. -materialize
這個operator的實現代碼很是簡單,可是仍是比較有用的,返回一個signal,這個新的signal將receiver的每一個event都轉換爲RACEvent對象而後發送,即便當receiver sendError:和sendComplete的時候,這個signal也是先發送一個RACEvent對象,而後纔會sendError:或者sendComplete。因此對於這個新的signal,只須要subscribeNext:便可,判斷收到的RACEvent的type就行,再也不須要再分別在next/error/block內處理不一樣的邏輯。
11. -bind:
bind:的源代碼仍是比較清晰易懂的,可是這個operator沒有具體的應用場景。bind:主要作的事情,按我本身的理解就是實現了多個signals的flatten操做,也就是將多個signals合併爲一個新的signal-of-signals類型的signal,以後每一個signal的next/error event都能被send到這個新的signal-of-signals之中。可是隻有當全部的signal都complete的時候,signal-of-signals的complete event纔會被send。包括flattenMap:在內的不少operator內部都是使用這個bind:實現的,因此這個operator是一個很是核心的operator,目前我寫RAC代碼並很少,可是感受這個操做符是使用signal-of-signals的核心。不過,這個operator的代碼其實是很是簡單的,值得閱讀。
參考資料:
<Functional Reactive Programming on iOS>
https://github.com/ReactiveCocoa/ReactiveCocoa/tree/master/Documentation
http://limboy.me/ios/2014/06/06/deep-into-reactivecocoa2.html
http://rcdp.io/Signal.html (翻譯:http://noark9.github.io/2015/01/25/rac-signal-from-rcdio/ )
http://spin.atomicobject.com/2015/03/19/reactivecocoa-asynchronous-libraries/
http://www.teehanlax.com/blog/getting-started-with-reactivecocoa/
http://www.raywenderlich.com/62699/reactivecocoa-tutorial-pt1,http://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt2
http://marksands.github.io/2015/1/7/reacting-to-reactive-cocoa-part-iii.html
http://spin.atomicobject.com/2014/02/03/objective-c-delegate-pattern/