ReactiveCocoa初識篇

關于ReactiveCocoa

ReactiveCocoa是iOS環(huán)境下的一個函數(shù)式響應式編程框架。函數(shù)式響應式編程(Functional Reactive Programming, FRP)這個概念由微軟的牛X團隊提出,ReactiveCocoa是受其啟發(fā)而誕生的框架,應用范圍非常廣泛。

ReactiveCocoa至今已經(jīng)發(fā)展出第4個版本了,而本文所針對的ReactiveCocoa版本是v2.5;照常例,下文將使用RAC代替ReactiveCocoa。

P.S: ReactiveCocoa 3.0已經(jīng)放棄對iOS 7的支持,因此如果支持iOS 7,必須得使用更低的版本。因此,若想通過CocoaPods安裝ReactiveCocoa v2.5,需要指明platform是低于v8.0的版本,譬如platform :ios, '7.0'。如果使用Swift語言,RxSwift似乎是更好的選擇。

在本文中,理解Signal是重頭戲,個人認為,結合Sequence理解信號(Signal)比較容易一些,因此會花一些篇幅闡述Sequence;除此之外,還有必要介紹高階函數(shù)和函數(shù)式編程。

P.S: Sequence在RAC中的地位越來越低,在Swift環(huán)境中,干脆被干掉了。

高階函數(shù)和函數(shù)式編程

從維基百科的解釋來看,一個高階函數(shù)需要滿足如下兩個條件:

  • 一個或者多個函數(shù)作為輸入;
  • 有一個函數(shù)作為輸出。

P.S: 維基百科代表不了權威,至少在高階函數(shù)這個問題上,網(wǎng)友存在分歧,有的人認為這兩個條件滿足一個即可;另一部分人認為二者皆不可缺。為了配合我所參考的資料 –iOS的函數(shù)響應型編程– 的說法,我只好選擇后者。

有必要在了解ReactiveCocoa之前認識一下函數(shù)式編程,可以參考函數(shù)式編程初探用RXCollections進行函數(shù)式編程

流和序列

RAC有一個核心概念叫流(Stream),它是data的序列化抽象。下文提到的LimBoy把Signal比喻成水管,我認為完全可以把Stream也比喻成水管,data就好像流淌在水管中的玻璃球,玻璃球直徑和水管內徑相仿,只能依次流過。以我們當前的認知水平,data的序列化就好像是一個數(shù)組或者列表。Data在序列中依次被排好序,它們能像水管中的玻璃球一樣流出來。

P.S: 「水管」和「玻璃球」比喻,我所知道的出處是LimBoy,下文再解釋Signal時會再次說明。

在RAC中,使用類RACStream抽象Stream,該類是個抽象類,本身不能被用來定義實例,用得更多的是它的兩個子類:RACSequence和RACSignal。換句話說,在RAC中,有兩種特定的流:序列(RACSequence)和信號(RACSignal)。

可以使用RAC為NSArray定義的category方法rac_sequence,將數(shù)組橋接為一個序列:

NSArray*array =@[@1,@2,@3,@4];
RACSequence*sequence1 = [arrayrac_sequence];

可以對生成的sequence1進行map、filter等處理,得到一個新的序列,前者對序列中的每個data進行處理,后者將序列中的data進行匹配過濾。下面例子先進行map處理,對序列中的data進行平方處理,得到一個新的序列sequence2,然后再對新序列進行filter處理,將偶數(shù)給剔除掉,得到sequence3,最后,再將sequence3還原為NSArray,并將所有元素給打印出來:

RACSequence *sequence2 = [sequence1 map:^id(NSNumber * value) {
    return [NSNumber numberWithInteger:value.integerValue*value.integerValue];
}];
RACSequence *sequence3 = [sequence2 filter:^BOOL(NSNumber * value) {
    return value.integerValue % 2 == 1;
}];
NSLog(@"%@", sequence3.array);
// print: (1, 9)

顯然,上述的兩個步驟其實可以鏈式串起來,這樣至少可以將sequence2這個臨時變量給省掉。
到了這里,應該對Sequence有了基本的理解了。

P.S: 文筆不佳,閱讀《iOS的函數(shù)響應型編程》的前兩章效果會更好。

P.S: 除了NSArray,還可以橋接為RACSequence的類型包括:NSDictionary、NSEnumerator、NSIndexSet、NSSet、RACSequence、RACTuple(RAC定義的一種類型,和其他語言譬如Swift中的Tuple類似)。

除了上面提到的map、filter,RAC的Stream還定義了其他很多基本操作:

- (instancetype)flattenMap:(RACStream * (^)(id value))block;
- (instancetype)flatten;
- (instancetype)map:(id (^)(id value))block;
- (instancetype)filter:(BOOL (^)(id value))block;
- (instancetype)ignore:(id)value;
- (instancetype)skip:(NSUInteger)skipCount;
- (instancetype)take:(NSUInteger)count;
+ (instancetype)zip:(id<NSFastEnumeration>)streams;

如上只是其中一部分,更過基本操作詳見RACStream.h。需要說明的是,上述的基本操作都是建立在如下API基礎之上,RACStream并未實現(xiàn)這些API,得由子類(即RACSequence和RACSignal)自己實現(xiàn)。

+ (instancetype)empty;
+ (instancetype)return:(id)value;
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;
- (instancetype)concat:(RACStream *)stream;
- (instancetype)zipWith:(RACStream *)stream;

P.S: RACStream定義的如上5個抽象方法,sunnyxx在其博客Reactive Cocoa Tutorial [2] = 百變RACStream里已有比較好的說明,可以參考一下。

如何看待上述的基本方法呢?這些方法(無論是類方法還是實例方法)都返回RACStream對象,這意味著可以在它們的基礎上進行鏈式調用,事實上達成了函數(shù)式編程的目的。

想想數(shù)學分支中的代數(shù),最基礎的運算無非是加減,在加減的基礎上引出了乘除,然后有了各種各樣更復雜的數(shù)學運算,譬如求導、微分、積分、卷積等等。對于Stream也一樣,在理解這些基本操作后,我們就可以基于鏈式調用實現(xiàn)各種復雜的邏輯。

至于這些操作的具體意義及用法,本文先略過,以后再說。

信號

在RAC中,Signal也是一種Stream,可以被綁定和傳遞。把Sequence想象成Stream并不是很難,但把Signal理解成Stream還是蠻有挑戰(zhàn)的。
如何理解Signal呢?能力有限,我也沒有更好的表述。如下是一段在ReactiveCocoa的中文世界里被廣泛傳播的解釋:

可以把信號想象成水龍頭,只不過里面不是水,而是玻璃球(data),直徑跟水管的內徑一樣,這樣就能保證玻璃球是依次排列,不會出現(xiàn)并排的情況(數(shù)據(jù)都是線性處理的,不會出現(xiàn)并發(fā)情況)。水龍頭的開關默認是關的,除非有了接收方(subscriber),才會打開。這樣只要有新的玻璃球進來,就會自動傳送給接收方??梢栽谒堫^上加一個過濾嘴(filter),不符合的不讓通過,也可以加一個改動裝置,把球改變成符合自己的需求(map)。也可以把多個水龍頭合并成一個新的水龍頭(combineLatest:reduce:),這樣只要其中的一個水龍頭有玻璃球出來,這個新合并的水龍頭就會得到這個球。

P.S: 這段摘自LimBoy的博文ReactiveCocoa與Functional Reactive Programming。

Sequence v.s Signal

Sequence和Signal都是Stream,但它們是不同類型的流。前者是pull-driven(拉驅動),后者是push-driven(推驅動)。所謂pull-driven,可以類比獲取網(wǎng)頁的方式,發(fā)起一個正確的HTTP請求,我們總會得到一些數(shù)據(jù),因為數(shù)據(jù)就在服務端的數(shù)據(jù)庫中躺著;而push-driven,可以類比推送(Push),數(shù)據(jù)并不是隨時都有的,客戶端也不知道什么時候該去獲取,只能與服務端保持長連接,當服務端有新數(shù)據(jù)時,就主動推送(Push)過來。

對于初學者,理解pull-driven和push-driven這兩個名詞不是很容易,但理解Sequence和Signal的區(qū)別還是不難:對于Sequence而言,在它被創(chuàng)立之初,其中的data(玻璃球)是被確定的,可以從流中把它們一個一個查詢出來;但對于Signal而言,在它(水管)被創(chuàng)立的時候,其中是沒有data(玻璃球)的,data是之后在某個時刻(譬如notification發(fā)生時、網(wǎng)絡請求完成時)才被放入的。
除了data驅動方式不同,Sequence和Signal所傳遞的data類型還不同,Sequence傳遞的是對象,Signal傳遞的是事件,無論是對象還是事件,在本文中都以data概述。

P.S: RACSequence和RACSignal可以互相轉化,詳見Signal

Signal和Subscriber

對于Sequence而言,在其創(chuàng)立之初,其中的data就是確定的,經(jīng)過一系列的操作(譬如map、filter)便可將同步將結果給取出來。

但對于Signal,不曉得什么時候才有data被放進去,顯然不能同步等待處理結果。因而需要有一種機制來解決這個問題。

Subscription就是來解決這個問題的,Subscriber是Subscription中的核心概念,RAC定義RACSubscriber來描述它。關于Subscription和Subscriber,官方文檔已經(jīng)有非常清晰的表述,詳見這里這里

P.S: 當把信號理解成時,「signal」這個名詞怎么看都覺得別扭,但當它和subscriber搭配時,卻又顯得那么和諧。

在描述signal和subscriber的關系時,Limboy使用插座和插頭分別來類比它們,插座(signal)負責取電,插頭(subscriber)負責使用電,一個插座可以插任意數(shù)量的插頭。當一個插座沒有插頭時,什么都不會干,處于冷(cold)狀態(tài),只有插了插頭才會去獲取電,此時處于熱(hot)狀態(tài)。

上文已經(jīng)提到Signal傳遞的data是event,它所傳遞的event包括3種:值事件、完成事件錯誤事件。其中在傳遞值事件時,可以攜帶數(shù)據(jù)。

落實到代碼層面,傳遞值事件完成事件以及錯誤事件的本質就是向subscriber發(fā)送sendNext:、sendComplete以及sendError:消息,如下代碼可以簡單描述它們的關系:

// 1. 創(chuàng)建信號(冷信號)
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    // block在什么時候被調用: 當信號被訂閱時調用;
  
    // 3. 模擬data(值事件)的產生
    [subscriber sendNext:@"Hello"];
    return nil;
}];

// 2. 訂閱信號(冷信號變熱信號)
[signal subscribeNext:^(id x) {
    // block什么時候被調用:當subscriber接收到sendNext:消息時;
    NSLog(@"%@", x);
}];

上述代碼中,步驟2在內部實現(xiàn)里創(chuàng)建一個RACSubscriber對象,該對象會被傳入到步驟3所對應的block中。

冷信號和熱信號

沒有被訂閱的信號被稱為冷信號,冷信號默認情況下什么都不干,換句話說,冷信號的subscription block永遠都不會被執(zhí)行,譬如:

RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    NSLog(@"triggered");
    [subscriber sendNext:@"hello, next"];
    [subscriber sendCompleted];
    return nil;
}];

這段代碼創(chuàng)建了一個signal,但因為沒有被subscribed,所以什么也不會發(fā)生。代碼中使用類方法createSignal:創(chuàng)建一個RACDynamicSignal(RACSignal的子類)對象,后者有一個名為didSubscribe的Block屬性,調用createSignal:傳入的實參block被賦予該屬性,當RACDynamicSignal被訂閱(subscribe)時,會回調該block。

P.S: RAC中的RACSignal以類簇的方式實現(xiàn),有點類似于Foundation中NSString、NSArray等,它定義了很多RACSignal子類,暫時不用理會這些子類,以后的博客中再詳細介紹。

Side Effect

如果某個RACSignal(以RACDynamicSignal為例)被多個subscriber訂閱,那么它的didSubscribe會被多次調用嗎?默認情況下是的,如下:

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    // didSubscribe block
    NSLog(@"triggered");
    [subscriber sendNext:@"test"];
    return nil;
}];

[signal subscribeNext:^(id x) {
    NSLog(@"subscriber No.1: %@", x);
}];

[signal subscribeNext:^(id x) {
  NSLog(@"subscriber No.2: %@", x);
}];

/* prints:
triggered
subscriber No.1: test
triggered
subscriber No.2: test
*/

顯然,didSubscribe被調用了兩次。或許這是你想要的結果,或許不是;更多的時候這不是我們想要的結果,即所謂的副作用(side effect)。如果想要避免這種情況的發(fā)生,可以使用reply方法,它的作用是保證signal只被觸發(fā)一次,然后把sendNext:的value給緩存起來,下一次再有新的subscriber時。直接發(fā)送緩存的value。如下:

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    // didSubscribe block
    NSLog(@"triggered");
    [subscriber sendNext:@"test"];
    return nil;
}];

signal = [signal replay];

[signal subscribeNext:^(id x) {
    NSLog(@"subscriber No.1: %@", x);
}];

[signal subscribeNext:^(id x) {
    NSLog(@"subscriber No.2: %@", x);
}];

/* prints:
triggered
subscriber No.1: test
subscriber No.2: test
*/

P.S: 我對side effects的理解有問題,把它單純想象成應該避免的負面東西,這是不對的。需要有更深刻的理解,得重寫一下!還得思考「side effects里一般放什么樣的代碼?」。

時至今日,ReactiveCocoa已經(jīng)和AFNetWorking一樣,變得非常大眾了,關于它的學習資料也不像前兩年那樣稀缺,如下是我認為質量比較高的學習資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容