Reactive Cocoa Tutorial = 只取所需的Filters

原文鏈接:http://www.cocoachina.com/industry/20140630/8985.html


概覽

 

簡而言之,Reactive Cocoa(RAC)就是一個函數響應式編程思想在Cocoa下的實現。
 
說說在RAC框架下做了一個項目的趕腳吧:
 
挺新鮮挺有意思,開發人員水平很高,框架封裝性和實用性一流,看了看人家對宏的使用發現原來用的純小兒科,對編譯器的控制,block的使用也很值得的學習。
 
編程思想上的一些改變。原創的一個可能也不大恰當的比喻:原來的編程思想像是“走迷宮”,RAC的編程思想是“建迷宮”。意思是,之前的編程思路是命令式,大概都是“程序啓動時執行xxxx,在用戶點擊後的回調函數執行xxx,收到一個Notification後執行xxx”等等,如同走迷宮一樣,走出迷宮需要在不同時間段記住不同狀態根據不同情況而做出一系列反應,繼而走出迷宮;相比下,RAC的思想是建立聯繫,像鐘錶中的齒輪組一樣,一個扣着一個,從轉動發條到指針走動,一個齒輪一個齒輪的傳導(Reactive),複雜但完整而自然。如同迷宮的建造者,在設計時早已決定了哪裏是通路,哪裏是死路或是哪個路口指向了出口,當一個挑戰者(Event)走入迷宮時(Signal),他一定會在設置好的迷宮中的某個路線行走(傳遞),繼而走到終點(Completion)或困死在裏面(Error)。
 
寫出代碼結構明顯不一樣。由於RAC將Cocoa中KVO、UIKit Event、delegate、selector等都增加了RAC支持,所以都不用去做很多跨函數的事,比如KVO個對象然後在回調裏面xxx,從storyboard裏面連個UIButton的IBAction出來xxx,或是設個UITextField的delegate出來去取輸入的文本xxx。但在RAC下就像上面比喻的建迷宮,把這些大都放在“-viewDidLoad:”就可以了,當然像UITableView的delegate和data source這麼大規模的代理模式就還是老老實實寫吧。
 
舉個例子:
 
我就想幹這麼個事:“一個label一個text field,下面輸啥上面顯示啥”
 
老寫法大概做法是這個vc實現UITextFieldDelegate協議,把這個text field的delegate設到vc上面,然後在要改變text的那個delegate方法裏面取當前text field的text值,再賦給label上;
 
使用RAC的話就一句話(當然得把這倆控件都IBOutlet出來):
  1. RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; 
看着就挺爽。複雜的例子先不舉了。
 
總之吧,等今後維護RAC的開發者和使用者把更多的Cocoa的東西歸入RAC的框架中,這個框架基本上都可以凌駕於Cocoa這個框架了,意思是甚至用不着知道那些delegate啊KVO啊蘋果告訴你是咋用的,用RAC封裝的就行了。RAC對於值的顯示大都是和property“綁定”的關係,像使用storyboard構建頁面時,對於有響應的控件基本都得IBOutlet出來作爲一個property,而不是像原來一樣連個IBAction出來或者連個delegate出來。對於視圖層到model層之間的綁定就顯得有些生硬了,相當於視圖直接耦合了model,於是應運而生M-V-VM結構,說白了就是在View和Model之間增加了一個ViewModel來解耦,這樣View裏面要做的基本就是綁定VM以及一些純視圖的操作(比如用什麼動畫效果展示一個數據);VM裏面是和View相關的數據部分的儲存和操作,比如說一個UITableView的data source,一個對email輸入合法性的驗證方法,當然還有的是對真正Model層的調用和結果的刷新,由於View已經和VM綁定,這樣VM在刷新的時候只刷新自己的屬性就得了。
 
其實重要的還是寫代碼思維方式的變化,如果全工程都使用RAC來實現,對於同一個業務邏輯終於可以在同一塊代碼裏完成了,將UI事件,邏輯處理,文件或數據庫操作,異步網絡請求,UI結果顯示,這一大套統統用函數式編程的思路嵌套起來,進入頁面時搭建好這所有的關係,用戶點擊後妥妥的等着這一套聯繫一個個的按期望的邏輯和次序觸發,最後顯示給用戶。感覺就像是搭好了一個精緻的遊樂場,然後不緊不慢地打開大門:@”Come on 熊孩子們!”
 
PS:寫這個blog的時候用的RAC版本是2.2.3,現在有更高的版本,所以還會有很多變化。
 
神奇的Macros
 
先說說RAC中必須要知道的宏:
  1. RAC(TARGET, [KEYPATH, [NIL_VALUE]]) 
使用:
  1. RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; 
  2.  
  3. RAC(self.outputLabel, text, @"收到nil時就顯示我") = self.inputTextField.rac_textSignal; 
這個宏是最常用的,RAC()總是出現在等號左邊,等號右邊是一個RACSignal,表示的意義是將一個對象的一個屬性和一個signal綁定,signal每產生一個value(id類型),都會自動執行:
  1. [TARGET setValue:value ?: NIL_VALUE forKeyPath:KEYPATH]; 
數字值會升級爲NSNumber *,當setValue:forKeyPath時會自動降級成基本類型(int, float ,BOOL等),所以RAC綁定一個基本類型的值是沒有問題的
  1. · RACObserve(TARGET, KEYPATH) 
作用是觀察TARGET的KEYPATH屬性,相當於KVO,產生一個RACSignal
 
最常用的使用,和RAC宏綁定屬性:
  1. RAC(self.outputLabel, text) = RACObserve(self.model, name); 
上面的代碼將label的輸出和model的name屬性綁定,實現聯動,name但凡有變化都會使得label輸出
  1. @weakify(Obj); 
  2. @strongify(Obj); 
這對宏在 RACEXTScope.h 中定義,RACFramework好像沒有默認引入,需要單獨import
 
他們的作用主要是在block內部管理對self的引用:
  1. @weakify(self); // 定義了一個__weak的self_weak_變量 
  2. [RACObserve(self, name) subscribeNext:^(NSString *name) { 
  3.     @strongify(self); // 局域定義了一個__strong的self指針指向self_weak 
  4.     self.outputLabel.text = name; 
  5. }]; 
這個宏爲什麼這麼吊,前面加@,其實就是一個啥都沒幹的@autoreleasepool {}前面的那個@,爲了顯眼罷了。
 
這兩個宏一定成對出現,先weak再strong
 
除了RAC中常用宏的使用,有一些宏的實現方法也很值得觀摩。
 
舉個高級點的栗子:
 
要乾的一件事,計算一個可變參數列表的長度。
 
第一反應就是用參數列表的api,va_start va_arg va_end遍歷一遍計算個和,但仔細想想,對於可變參數這個事,在編譯前其實就已經確定了,代碼裏括號裏有多少個參數一目瞭然。
 
RAC中Racmetamarcos.h中就有一系列宏來完成這件事,硬是在預處理之後就拿到了可變參數個數:
  1. #define metamacro_argcount(...) \ 
  2.     metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) 
這個宏由幾個工具宏一層層展開,現在模擬一下展開過程:
 
假如我們要計算的如下:
  1. int count = metamacro_argcount(a, b, c); 
於是乎第一層展開後:
  1. int count = metamacro_at(20, a, b, c, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) 
再看metamacro_at的定義:
  1. #define metamacro_at(N, ...) metamacro_concat(metamacro_at, N)(__VA_ARGS__) 
  2. // 下面是metamacro_concat做的事(簡寫一層) 
  3. #define metamacro_concat_(A, B) A ## B 
於是乎第二層展開後:
  1. int count = metamacro_at20(a, b, c, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); 
再看metamacro_at20這個宏乾的事兒:
  1. #define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__) 
於是乎第三層展開後,相當於截斷了前20個參數,留下剩下幾個:
  1. int count = metamacro_head(3, 2, 1); 
這個metamacro_head:
  1. #define metamacro_head(...) metamacro_head_(__VA_ARGS__, 0) 
  2. #define metamacro_head_(FIRST, ...) FIRST 
後面加個0,然後取參數列表第一個,於是乎:
  1. int count = 3; 
大功告成。
 
反正我看完之後感覺挺震驚,宏還能這麼用,這樣帶來的好處不止是將計算在預處理時搞定,不拖延到運行時噁心cpu;但更重要的是編譯檢查。比如某些可變參數的實現要求可以填2個參數,可以填3個參數,其他的都不行,這樣,也只有這樣的宏的實現,才能在編譯前就確定了錯誤。
 
除了上面,還有一個神奇的宏的使用:
 
當使用諸如RAC(self, outputLabel)或RACObserve(self, name)時,發現寫完逗號之後,輸入第二個property的時候會出現完全正確的代碼提示!這相當神奇。
探究一下,關鍵的關鍵是如下一個宏:
  1. #define keypath(...) \ 
  2.     metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__)) 
這個metamacro_argcount上面說過,是計算可變參數個數,所以metamacro_if_eq的作用就是判斷參數個數,如果個數是1就執行後面的keypath1,若不是1就執行keypath2。
 
所以重點說一下keypath2:
  1. #define keypath2(OBJ, PATH) \ 
  2.     (((void)(NO && ((void)OBJ.PATH, NO)), # PATH)) 
乍一看真挺懵,先化簡,由於Objc裏面keypath是諸如”outputLabel.text”的字符串,所以這個宏的返回值應該是個字符串,可以簡化成:
  1. #define keypath2(OBJ, PATH) (???????, # PATH) 
先不管”??????”是啥,這裏不得不說C語言中一個不大常見的語法(第一個忽略):
  1. int a = 0, b = 0; 
  2. a = 1, b = 2; 
  3. int c = (a, b); 
這些都是逗號表達式的合理用法,第三個最不常用了,c將被b賦值,而a是一個未使用的值,編譯器會給出warning。
 
去除warning的方法很簡單,強轉成void就行了:
  1. int c = ((void)a, b); 
再看上面簡化的keypath2宏,返回的就是PATH的字符串字面值了(單#號會將傳入值轉成字面字符串)
  1. (((void)(NO && ((void)OBJ.PATH, NO)), # PATH)) 
對傳入的第一個參數OBJ和第二個正要輸入的PATH做了點操作,這也正是爲什麼輸入第二個參數時編輯器會給出正確的代碼提示。強轉void就像上面說的去除了warning。
 
但至於爲什麼加入與NO做&&,我不太能理解,我測試時其實沒有時已經完成了功能,可能是作者爲了屏蔽某些隱藏的問題吧。
 
這個宏的巧妙的地方就在於使得編譯器以爲我們要輸入“點”出來的屬性,保證了輸入值的合法性(輸了不存在的property直接報錯的),同時利用了逗號表達式取逗號最後值的語法返回了正確的keypath。
 
總之
RAC對宏的使用達到了很高的水平,還有諸如RACTuplePack,RACTupleUnpack的宏就不細說了,值得研究。
 
PS:上面介紹的metamacro和@strongify等宏確切來說來自RAC依賴的extobjc,作者是Justin Spahr-Summers,正是RAC作者之一。
 
百變RACStream
 
在RAC下開發乾的最多的事就是建立RACSignal和subscribe RACSignal了,它是RAC的核心所在。本篇介紹了RAC的運作原理和設計思路,從函數式編程形成的RACStream繼而介紹它的子類 - RAC最核心的部分RACSignal。
 
函數式編程
我們知道Reactive Cocoa是函數式編程(Functional Programing)(FP)思想的實現。FP有一套成熟的理論,這裏只講講我個人理解吧。
 
我覺得FP就是“像計算函數表達式一樣來解決一個問題”,舉個栗子,中學題:
  1. 已知:f(x) = 2sin(x + π/2), 求 f(π/2)的值。 
其中x是這個函數的輸入,f(x)爲計算的輸出結果,求f(π/2)時給定了x自然能計算出個結果來(說實話我真忘了咋算了)
 
當然,仔細看這個函數,其實是可以分解成幾個小函數的:
  1. f1(x) = x + π/2 
  2. f2(x) = sin(x) 
  3. f3(x) = 2x 
而原來的f(x)可以被小函數組合:
  1. f(x) = f3(f2(f1(x))) 
所以不難得出這麼個推論:要是我手上有足夠的基本函數,我就能用上面的組合的方法組合出任意一個複雜的函數了。再想想事實上這些年來學數學的過程不就是在一個個積累基本函數的過程嘛,從基本運算,到三角函數,到乘方開方,再到微積分。基本函數越來越多,能解決的數學問題也越來越複雜。
 
再來看一個函數是怎麼構成的,FP理論裏叫monads,十分抽象,沒讀懂,但能理解出來:一個函數只要有一個對於輸入值的運算方法和一個返回值,就夠了。也容易理解,給它一個輸入,乾點事情,給出一個輸出,就行了,當然現實情況要複雜得多(比如說輸出值本身就是個函數?)有些函數是有輸入的條件的,比如原來數學解個函數時候經常跟個作用域或者限制條件,比如f(x) = 10 / x , (x不爲0),要是傳個0這個函數就認爲計算錯誤。
 
對於像上面栗子的函數,每個函數都能接收上一個函數輸出的結果,作爲自己的輸入,這樣才能嵌套生成最終結果,同時,計算的順序也是一定從裏向外,所以換個寫法可以寫成:
  1. start ---x--> f1(x) --(temp value1)--> f2(temp value1) --(temp value2)--> f3(temp value2) ---> result 
於是乎嵌套就被表示成了序列,來個高大上的名字怎麼樣,就叫流(Stream)
 
RACStream
這就是RACStream所表示的含義。
 
按照上面說的,其實RACStream的名字有點點歧義,對於一個RACStream對象,它在意義上等同於上面的f1(x),f2(x),f3(x),而不是那一大串整體,表示整體的應該是最外層的和f(x)對應的那個對象,叫個RACStreamComponent比較好?理解時候得注意下。
 
所以作爲一個基本函數的RACStream應該至少應該有:
 
1、怎麼傳入值
2、怎麼返回值
3、怎麼與其他函數組合
4、怎麼實現函數的作用域(監測輸入值來做處理)
5、這函數叫啥- -
 
得益於在Objc下實現,所以輸入輸出的“值”都用個id類型就行了,遇到多個值的組合就用RACTurple(可以把多個值壓包和解包,類比WINRAR),1和2解決
 
RACStream從實例變量來看只有一個name,當然它也只應該有個name - -,5解決
 
裏面重點問題就是上面的3和4了。由於函數組合之後仍然是個函數,所以也很容易理解兩個Stream對象的組合其實就是生成一個新的Stream對象,它返回了分別由兩個子Stream先後運算產生的最終結果
 
觀摩一下RACStream定義的基本方法:
  1. + (instancetype)empty; 
  2. + (instancetype)return:(id)value; 
  3. - (instancetype)bind:(RACStreamBindBlock (^)(void))block; // for 4 
  4. - (instancetype)concat:(RACStream *)stream; // for 3 
  5. - (instancetype)zipWith:(RACStream *)stream; // for 3 
RACStream作爲一個描述抽象的父類,這幾個基本方法並沒有實現,是由具體子類來實現,RACStream的兩個子類分別是RACSignal和RACSequence
 
+empty 是一個不返回值,立刻結束(Completed)的函數,意思是執行它之後除了立刻結束啥都不會發生,可以理解爲RAC裏面的nil。
+return: 是一個直接返回給定值,然後立刻結束的函數,比如 f(x) = 213
-bind:是一個非常重要的函數,在Rac Doc中被描述爲‘basic primitives, particularly’,它是RACStream監測“值”和控制“運行狀態”的基本方法,個人認爲看註釋文檔不能理解它是幹嘛的,而且bind英語“捆綁,綁定,強迫,約束”這幾個意思也感覺對不上,我覺得叫“綁架”倒是更貼切一點。在-bind:之後,之前的RACStream就處於被“綁架”的狀態,被綁架的RACStream每產生一個值,都要經過“綁架者”來決定:
 
1、是否使這個RACStream結束(被綁架者是否還能繼續活着)
 
2、用什麼新的RACStream來替換被綁架的RACStream,傳出的結果也成了新RACStream產生的值(綁匪可以選擇再抓一個人質放之前那個前面)
 
舉個具體栗子,RACStream的 - take:方法,這個方法使一個RACStream只取前N次的值(有縮減):
  1. - (instancetype)take:(NSUInteger)count { 
  2.     Class class = self.class
  3.      
  4.     return [[self bind:^{ // self被綁架 
  5.         __block NSUInteger taken = 0; 
  6.  
  7.         return ^ id (id value, BOOL *stop) { // 這個block在被綁架的self每輸出一個值得時候觸發 
  8.             RACStream *result = class.empty; 
  9.  
  10.             if (taken < count) result = [class return:value]; // 未達到N次時將原值原原本本的傳遞出去 
  11.             if (++taken >= count) *stop = YES; // 達到第N次值後幹掉了被綁架的self 
  12.  
  13.             return result; // 將被綁架的self替換爲result 
  14.         }; 
  15.     }]]; 
-concat: 和 -zipWith: 就是將兩個RACStream連接起來的基本方法了:
 
[A concat:B]中A和B像皇上和太子的關係,A是皇上,B是太子。皇上健在的時候統治天下發號施令(value),太子就候着,不發號施令(value),當皇上掛了(completed),太子登基當皇上,此時發出的號令(value)是太子的。
[C zipWith:D]可以比喻成一對平等恩愛的夫妻,兩個人是“綁在一起“的關係來組成一個家庭,決定一件事(value)時必須兩個人都提出意見(當且僅當C和D同時都產生了值的時候,一個value才被輸出,CD只有其中一個有值時會掛起等待另一個的值,所以輸出都是一對值(RACTuple)),當夫妻只要一個人先掛了(completed)這個家庭(組合起來的RACStream)就宣佈解散(也就是無法湊成一對輸出時就終止)
 
除了上面幾個基本方法,RACStream還有不少的Operation方法,這些操作方法的實現大都是組合基本的方法來達到特定的目的,雖然是RACStream這個基類實現的,但我覺得還是放在後面介紹RACSignal的時候作爲它的使用方法來說比較合適,畢竟絕大多數編程的對象的都是RACStream的兩個子類,後面再展開介紹好了。
 
RACSignal的巧克力工廠
 
上面介紹了函數式編程和RACStream,使得函數得以串聯起來,而它的具體子類,也是RAC編程中最重要的部分,RACSignal就是使得算式得以逐步運算並使其有意義的關鍵所在,本節主要介紹RACSignal的機理,具體的使用放到接下來的幾節。
 
巧克力工廠的運作模式
RACStream實現了一個嵌套函數的結構,如f(x) = f1(f2(f3(x))),但好像是考試卷子上的一道題,沒有人去做它,沒得出個結果的話這道題是沒有意義的。
 
OK,現在起將這個事兒都比喻成一個巧克力工廠,f(x)的結果是一塊巧克力,f1,f2,f3代表巧克力生產的幾個步驟,如果這個工廠不開工,它是沒有意義的。
 
再說RACSignal,引用RAC doc的描述: “A signal, represented by the RACSignal class, is a push-driven stream.”
 
我覺得這個push-driven要想解釋清楚,需要和RACSequence的pull-driven放在一起來看。在巧克力工廠,push-driven是“生產一個吃一個”,而pull-driven是“吃完一個才生產下一個”,對於工廠來說前者是主動模式:生產了巧克力就“push”給各個供銷商,後者是被動模式:各個供銷商過來“pull”產品時纔給你現做巧克力。
 
Status
 
所以,對於RACSigna的push-driven的生產模式,首先,當工廠發現沒有供銷商籤合同準備要巧克力的時候,工廠當然沒有必要開動生產;只要當有一個以上準備收貨的經銷商時,工廠纔開動生產。這就是RACSignal的休眠(cold)和激活(hot)狀態,也就是所謂的冷信號和熱信號。一般情況下,一個RACSignal創建之後都處於cold狀態,有人去subscribe才被激活。
 
Event
RACSignal能產生且只能產生三種事件:next、completed,error。
 
next表示這個Signal產生了一個值(成功生產了一塊巧克力)
 
completed表示Signal結束,結束信號只標誌成功結束,不帶值(一個批次的訂單完成了)
 
error表示Signal中出現錯誤,立刻結束(一個機器壞了,生產線立刻停止運轉)
 
工廠廠長存了所有供銷商的QQ,每當發生上面三件事情的一件時,都用QQ挨個兒發消息告訴他們,於是供銷商就能根據生產狀態決定要做點什麼。當訂單完成或者失敗後,廠長就會把這個供銷商的QQ刪了,以後發消息的時候也就沒必要通知他了。
 
Side Effects
RACSignal在被subscribe的時候可能會產生副作用,先舉個官方的栗子:
  1. __block int aNumber = 0; 
  2.  
  3. // Signal that will have the side effect of incrementing `aNumber` block 
  4. // variable for each subscription before sending it. 
  5. RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) { 
  6.     aNumber++; 
  7.     [subscriber sendNext:@(aNumber)]; 
  8.     [subscriber sendCompleted]; 
  9.     return nil; 
  10. }]; 
  11.  
  12. // This will print "subscriber one: 1" 
  13. [aSignal subscribeNext:^(id x) { 
  14.     NSLog(@"subscriber one: %@", x); 
  15. }]; 
  16.  
  17. // This will print "subscriber two: 2" 
  18. [aSignal subscribeNext:^(id x) { 
  19.     NSLog(@"subscriber two: %@", x); 
  20. }]; 
上面的signal在作用域外部引用了一個int變量,同時在signal的運算過程中作爲next事件的值返回,這就造成了所謂的副作用,因爲第二個訂閱者的訂閱而影響了輸出值。
 
 
我的理解來看,這個事兒做的就不太地道,一個正經的函數式編程中的函數是不應該因爲進行了運算而導致後面運算的值不統一的。但對於實際應用的情況來看也到無可厚非,比如用戶點擊了“登錄”按鈕,編程時把登錄這個業務寫爲一個login的RACSignal,當然,第一次調用登錄和再點一次第二次調用登錄的結果肯定不一樣了。所以說RAC式編程減少了大部分對臨時狀態值的定義,但不是全部哦。
 
怎麼辦呢?我覺得最好的辦法就是“約定”,RAC design guide裏面介紹了對於一個signal的命名法則:“Hot signals without side effects 最好使用property,如“textChanged”,不太理解什麼情況用到這個,權當做一個靜態的屬性來看就行。
Cold signals without side effects 使用名詞類型的方法名,如“-currentText”,“currentModels”,同時表明了返回值是個啥(這個尤其得注意,RACSignal的next值是id類型,所以全得是靠約定才知道具體返回類型)
Signals with side effects 這種就是像login一樣有副作用的了,推薦使用動詞類型的方法名,用對動詞基本就能知道是不是有副作用了,比如“-loginSignal”和“-saveToFile”大概就知道前面一個很可能有副作用,後面一個多存幾次文件應該沒副作用。”
 
當然,也可以multicast一個event,使得某些特殊的情況來共享一個副作用,後面再具體講,先一個官方的簡單的栗子:
  1. // This signal starts a new request on each subscription. 
  2. RACSignal *networkRequest = [RACSignal createSignal:^(id<RACSubscriber> subscriber) { 
  3.     AFHTTPRequestOperation *operation = [client 
  4.         HTTPRequestOperationWithRequest:request 
  5.         success:^(AFHTTPRequestOperation *operation, id response) { 
  6.             [subscriber sendNext:response]; 
  7.             [subscriber sendCompleted]; 
  8.         } 
  9.         failure:^(AFHTTPRequestOperation *operation, NSError *error) { 
  10.             [subscriber sendError:error]; 
  11.         }]; 
  12.  
  13.     [client enqueueHTTPRequestOperation:operation]; 
  14.     return [RACDisposable disposableWithBlock:^{ 
  15.         [operation cancel]; 
  16.     }]; 
  17. }]; 
  18.  
  19. // Starts a single request, no matter how many subscriptions `connection.signal` 
  20. // gets. This is equivalent to the -replay operator, or similar to 
  21. // +startEagerlyWithScheduler:block:. 
  22. RACMulticastConnection *connection = [networkRequest multicast:[RACReplaySubject subject]]; 
  23. [connection connect]; 
  24.  
  25. [connection.signal subscribeNext:^(id response) { 
  26.     NSLog(@"subscriber one: %@", response); 
  27. }]; 
  28.  
  29. [connection.signal subscribeNext:^(id response) { 
  30.     NSLog(@"subscriber two: %@", response); 
  31. }]; 
當地一個訂閱者subscribeNext的時候觸發了AFNetworkingOperation的創建和執行,開始網絡請求,此時又來了個訂閱者訂閱這個Signal,按理說這個網絡請求會被“副作用”,重新發一遍,但做了上面的處理之後,這兩個訂閱者接收到了同樣的一個請求的內容。
 
RACScheduler - 生產線
RACScheduler是RAC裏面對線程的簡單封裝,事件可以在指定的scheduler上分發和執行,不特殊指定的話,事件的分發和執行都在一個默認的後臺線程裏面做,大多數情況也就不用動了,有一些特殊的signal必須在主線程調用,使用-deliverOn:可以切換調用的線程。
 
但值得特殊瞭解的事實是:However, RAC guarantees that no two signal events will ever arrive concurrently. While an event is being processed, no other events will be delivered. The senders of any other events will be forced to wait until the current event has been handled.
 
意思是訂閱者執行時的block一定非併發執行,也就是說不會執行到一半被另一個線程進入,也意味着寫subscribeXXX block的時候沒必要做加鎖處理了。
 
巧克力的生產工藝
RACSignal的廠子建好了,運行的模式也都想好了,剩下的就是巧克力的加工工藝了。
 
有了RACStream的嵌套和組裝的基礎,RACSignal得以使用組件化的工藝來一步步的加工巧克力,從可可,牛奶,糖等原料,混合到這種巧克力適用的液態巧克力,過濾,提純,冷卻,夾心,壓模,再到包裝,一個巧克力就產出了。對於不同種類的巧克力,比如酒心巧克力,也不過是把其中的某個組件替換成注入酒心罷了。
 
RACSignal的生產組件,也就是它的各式各樣的operation,一個具體業務邏輯的實現,其實也就是選擇合適operation按合適的順序組合起來。
 
還舉那個用戶在textFiled輸入並顯示到上面的label中的栗子:
  1. RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; 
現在需求變成“用戶輸入3個字母以上才輸出到label,當不足3個時顯示提示”,OK,好辦:
  1. RAC(self.outputLabel, text) = [[self.inputTextField.rac_textSignal 
  2.     startWith:@"key is >3"/* startWith 一開始返回的初始值 */ 
  3.     filter:^BOOL(NSString *value) { 
  4.         return value.length > 3; /* filter使滿足條件的值才能傳出 */ 
  5. }]; 
需求又增加成“當輸入sunny時顯示輸入正確”
  1. RAC(self.outputLabel, text) = [[self.inputTextField.rac_textSignal 
  2.     startWith:@"key is >3"// startWith 一開始返回的初始值 
  3.     filter:^BOOL(NSString *value) { // filter使滿足條件的值才能傳出 
  4.         return value.length > 3; 
  5.     }] 
  6.     map:(NSString *value) { // map將一個值轉化爲另一個值輸出 
  7.         return [value isEqualToString:@"sunny"] ? @"bingo!" : value; 
  8.     }]; 
可以看出,基本上一個業務邏輯經過分析後可以拆解成一個個小RACSignal的組合,也就像生產巧克力的一道道工藝了。上面的栗子慢慢感覺就像了一個簡陋的輸答案的框了。
 
只取所需的Filters
 
RAC中的Filters
 
畫個範圍
一個Signal源可以產生一系列next值,但並非所有值都是需要的,具體的Subscriber可以選擇在原有Signal上套用Filter操作來過濾掉不需要的值。
我的定義:RAC中如果一個Operation將處理後的值集合是處理前值集合的子集,我們就可以把它歸爲Filter類型。
 
當然通過之前介紹的基礎操作完全可以自己拼出個想要的filter來,RAC爲了方便使用已經實現了幾個常用的filter,經過總結,這些filter大概可以分成兩類:next值過濾類型和起止點過濾類型
 
值過濾類型Filters
 
- filter: (BOOL (^)(id value))
RAC中的filter同名方法- filter:(BOOL (^)(id value)),簡單明瞭,將一個value用block做test,返回YES的纔會通過,它的內部實現使用了- flattenMap:,將原來的Signal經過過濾轉化成只返回過濾值的Signal,用法也不難理解:
  1. [[self.inputTextField.rac_textSignal filter:^BOOL(NSString *value) { 
  2.     return [value hasPrefix:@"sunny"]; 
  3. }] subscribeNext:^(NSString *value) { 
  4.     NSLog(@"This value has prefix `sunny` : %@", value); 
  5. }]; 
此外,還有幾個這個方法的衍生方法:
 
- ignore: (id)
忽略給定的值,注意,這裏忽略的既可以是地址相同的對象,也可以是- isEqual:結果相同的值,也就是說自己寫的Model對象可以通過重寫- isEqual:方法來使- ignore:生效。常用的值的判斷沒有問題,如下:
  1. [[self.inputTextField.rac_textSignal ignore:@"sunny"] subscribeNext:^(NSString *value) { 
  2.     NSLog(@"`sunny` could never appear : %@", value); 
  3. }]; 
 
- ignoreValues
這個比較極端,忽略所有值,只關心Signal結束,也就是隻取Comletion和Error兩個消息,中間所有值都丟棄。
注意,這個操作應該出現在Signal有終止條件的的情況下,如rac_textSignal這樣除dealloc外沒有終止條件的Signal上就不太可能用到。
 
- distinctUntilChanged
也是一個相當常用的Filter(但它不是- filter:的衍生方法),它將這一次的值與上一次做比較,當相同時(也包括- isEqual:)被忽略掉。
比如UI上一個Label綁定了一個值,根據值更新顯示的內容:
  1. RAC(self.label, text) = [RACObserve(self.user, username) distinctUntilChanged]; 
  2. self.user.username = @"sunnyxx"// 1st 
  3. self.user.username = @"sunnyxx"// 2nd 
  4. self.user.username = @"sunnyxx"// 3rd 
如果不增加distinctUntilChanged的話對於連續的相同的輸入值就會有不必要的處理,這個栗子只是簡單的UI刷新,但遇到如寫數據庫,髮網絡請求的情況時,代價就不能購忽略了。
 
所以,對於相同值可以忽略的情況,果斷加上它吧。
 
起止點過濾類型
 
除了被動的當next值來的時候做判斷,也可以主動的提前選擇開始和結束條件,分爲兩種類型:
take型(取)和skip型(跳)
 
- take: (NSUInteger)
從開始一共取N次的next值,不包括Competion和Error,如:
  1. [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
  2.     [subscriber sendNext:@"1"]; 
  3.     [subscriber sendNext:@"2"]; 
  4.     [subscriber sendNext:@"3"]; 
  5.     [subscriber sendCompleted]; 
  6.     return nil; 
  7. }] take:2] subscribeNext:^(id x) { 
  8.     NSLog(@"only 1 and 2 will be print: %@", x); 
  9. }]; 
 
- takeLast: (NSUInteger)
取最後N次的next值,注意,由於一開始不能知道這個Signal將有多少個next值,所以RAC實現它的方法是將所有next值都存起來,然後原Signal完成時再將後N個依次發送給接收者,但Error發生時依然是立刻發送的。
 
- takeUntil:(RACSignal *)
當給定的signal完成前一直取值。最簡單的栗子就是UITextField的rac_textSignal的實現(刪減版本):
  1. - (RACSignal *)rac_textSignal { 
  2.     @weakify(self); 
  3.     return [[[[[RACSignal 
  4.         concat:[self rac_signalForControlEvents:UIControlEventEditingChanged]] 
  5.         map:^(UITextField *x) { 
  6.             return x.text; 
  7.         }] 
  8.         takeUntil:self.rac_willDeallocSignal] // bingo! 
也就是這個Signal一直到textField執行dealloc時才停止
 
- takeUntilBlock:(BOOL (^)(id x))
對於每個next值,運行block,當block返回YES時停止取值,如:
  1. [[self.inputTextField.rac_textSignal takeUntilBlock:^BOOL(NSString *value) { 
  2.     return [value isEqualToString:@"stop"]; 
  3. }] subscribeNext:^(NSString *value) { 
  4.     NSLog(@"current value is not `stop`: %@", value); 
  5. }]; 
 
- takeWhileBlock:(BOOL (^)(id x))
上面的反向邏輯,對於每個next值,block返回 YES時才取值
 
- skip:(NSUInteger)
從開始跳過N次的next值,簡單的栗子:
  1. [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
  2.     [subscriber sendNext:@"1"]; 
  3.     [subscriber sendNext:@"2"]; 
  4.     [subscriber sendNext:@"3"]; 
  5.     [subscriber sendCompleted]; 
  6.     return nil; 
  7. }] skip:1] subscribeNext:^(id x) { 
  8.     NSLog(@"only 2 and 3 will be print: %@", x); 
  9. }]; 
 
- skipUntilBlock:(BOOL (^)(id x))
和- takeUntilBlock:同理,一直跳,直到block爲YES
 
- skipWhileBlock:(BOOL (^)(id x))
和- takeWhileBlock:同理,一直跳,直到block爲NO
 
總結
 
本章介紹了RAC中Filter類型的Operation,總結一下:
 
適用場景:需要一個next值集合的子集時
Filter類型:值過濾型和起止點過濾型
值過濾型常用方法: -filter:,-ignore:,-distinctUnitlChanged
起止點過濾型常用方法:take系列和skip系列
 
References
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章