什么是響應式編程
響應式編程是一種面向數據流和變化傳播的編程范式。這意味着可以在編程語言中很方便地表達靜態或動態的數據流,而相關的計算模型會自動將變化的值通過數據流進行傳播。
例如,在命令式編程環境中,a:=b+c表示將表達式的結果賦給a,而之后改變b或c的值不會影響a。但在響應式編程中,a的值會隨着b或c的更新而更新。
電子表格程序就是響應式編程的一個例子。單元格可以包含字面值或類似"=B1+C1"的公式,而包含公式的單元格的值會依據其他單元格的值的變化而變化。
響應式編程最初是為了簡化交互式用戶界面的創建和實時系統動畫的繪制而提出來的一種方法,但它本質上是一種通用的編程范式。
什么是ReactiveCocoa
ReactiveCocoa (RAC) 是一個Objective-C語言內實現響應式編程的框架。
RAC提供了大量的能夠完成發送 value's stream 的API。
RAC通過使用信號量(RACSignal)來完成獲取當前值和未來值的功能,而不像傳統的程序開發一樣需要聲明大量的變量。
Josh Abernathy這樣解釋它:
- 程序接收輸入產生輸出。輸出就是對輸入做了一些事的結果。輸入,轉換,輸出,完成。
- 輸入是應用動作的全部來源。點擊、鍵盤事件、定時器事件、GPS時間、網絡請求響應都算是輸入。這些事件被傳遞到應用中,應用將他們以某種方式混合,產生了結果:就是輸出。
- 輸出通常會改變應用的UI。開關狀態變化、列表有了新的元素都是UI變化。也有可能讓磁盤上某個文件產生變化,或者產生一個API請求,這都是應用的輸出。
- 但不像傳統的輸入輸出設計,應用的輸入輸出可以產生很多次。應用打開后,不只是一個簡單的 輸入→工作→輸出 就構成了一個生命周期。應用經常有大量的輸入並基於這些輸入產生輸出。
基本的使用方法
例如,我們目前想要實現一個NSString對象可以一直綁定到最新的時間,即使字符串發生了變化,也不應該是再去使用時間去重新賦值了。
聽起來特別像Objective-C語言內的KVO特性,但是這並不是具有壓倒性優勢的那個方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
那么我們現在使用RAC框架來實現這個功能
***.h
//用來標識時間變量 @property (nonatomic ,strong) NSDate *time; //用來標識文字顯示區域 @property (nonatomic ,strong) IBOutlet UILabel *label;
***.m
//申請注冊一個每個1秒將會在主線程執行一次的信號量 RACSignal *repeatSignal = [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] repeat]; //為信號量添加執行代碼端 [repeatSignal subscribeNext: ^(NSDate* time){ self.time = time; }]; //申請注冊一個時間屬性的信號量 RACSignal *timeSignal = [self rac_valuesForKeyPath:@"time" observer:self]; //為信號量添加執行代碼端 [timeSignal subscribeNext:^(NSDate* time) { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@"HH:mm:ss"]; self.label.text = [formatter stringFromDate:time]; RELEASESAFELY(formatter); }];
同樣和Objective-C語言內的KVO特性不同的是,RACSignal信號量可以進行過濾設置。
以上邊的例子的話,我們加一個功能。
•獲取偶數秒的時間
那么信號量部分的代碼可以寫為
//申請注冊一個時間屬性的信號量 RACSignal *timeSignal = [self rac_valuesForKeyPath:@"time" observer:self]; //為信號量添加過濾block [[timeSignal filter:^BOOL(NSDate* time) { //獲取描述的時間 NSDateComponents *com = [[NSCalendar currentCalendar] components:NSCalendarUnitSecond fromDate:time]; return com.second % 2 == 0; }] subscribeNext:^(NSDate* time) { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@"HH:mm:ss"]; self.label.text = [formatter stringFromDate:time]; RELEASESAFELY(formatter); }];
信號量還可以用來導出對應的狀態。與Objective-C語言KVO特性不同的是,RAC能夠為新的值設置其他的屬性。
那么我們還是舉個功能例子
•在注冊用戶時,當用戶密碼與確認密碼相同時,在Label中顯示"1",不相同時,顯示"0";
•如圖所示
傳統方式代碼
- (BOOL)isValid { return [self.password.text length] > 0 && [self.confirm.text length] > 0 && [self.password.text isEqual:self.confirm.text]; } #pragma mark - UITextFieldDelegate - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { self.label.text = @(self.isValid).description; return YES; }
我們發現邏輯被放在了很多方法里,零碎地擺放在view controller里,通過到處散布到delegate里的self.label.text = @(self.isValid).description;
方法在頁面的生命周期中被調用。
那么RAC實現方式的代碼
RACSignal *passworkSignal = self.password.rac_textSignal; RACSignal *confirmSignal = self.confirm.rac_textSignal; RACSignal *combineSignal = [RACSignal combineLatest:@[passworkSignal,confirmSignal] reduce:^(NSString *password, NSString *confirm){ ; return @([password isEqualToString:confirm]).description; }]; RAC(self,label.text) = combineSignal;
所有對於的輸入都整合在了一起。每次不論哪個輸入框被修改了,用戶的輸入都會被reduce成一個字符串的值,然后就可以自動來控制注冊按鈕的可用狀態了。
RAC除了能夠完成KVO的功能之外,同樣可以完成按鈕等用戶響應的交互功能
•完成一個點擊按鈕彈出Alert的功能
•如圖所示
傳統方式實現的代碼
- (void)viewDidLoad { [super viewDidLoad]; //添加觸發事件 [self.btn addTarget:self action:@selector(didClick) forControlEvents:UIControlEventTouchUpInside]; } //點擊按鈕觸發的回調方法 - (void)didClick { //創建彈出窗口 UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"藍鷗" message:nil delegate:nil cancelButtonTitle:nil otherButtonTitles:@"確定", nil]; [alertView show]; RELEASESAFELY(alertView); }
RAC方式實現的代碼如下
//添加觸發信號量 self.btn.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { //創建彈出窗口 UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"藍鷗" message:nil delegate:nil cancelButtonTitle:nil otherButtonTitles:@"確定", nil]; [alertView show]; RELEASESAFELY(alertView); return [RACSignal empty]; }];
通過以上的代碼,RACSignal信號量具有如下功能
- 異步控制或事件驅動的數據源:Cocoa編程中大多數時候會關注用戶事件或應用狀態改變產生的響應。
- 鏈式以來操作:網絡請求是最常見的依賴性樣例,前一個對server的請求完成后,下一個請求才能構建。
- 並行獨立動作:獨立的數據集要並行處理,隨后還要把他們合並成一個最終結果。這在Cocoa中很常見,特別是涉及到同步動作時。
RACSignal會觸發它們的subscriber三種不同類型的事件:
- 下一個事件從stream中提供一個新值。不像Cocoa集合,它是完全可用的,甚至一個signal可以包含
nil
。 - 錯誤事件會在一個signal結束之前被標示出來這里有一個錯誤。這種事件可能包含一個
NSError
對象來標示什么發生了錯誤。錯誤必須被特殊處理——錯誤不會被包含在stream的值里面。 - 完成事件標示signal成功結束,不會再有新的值會被加入到stream當中。完成事件也必須被單獨控制——它不會出現在stream的值里面。
一個RACSignal信號量的生命周期由很多下一個(next)
事件和一個錯誤(error)
或完成(completed)
事件組成(后兩者不同時出現)。
總結對比
RAC 與 KVO
Key-Value Observing是Cocoa所有魔法的核心,它被廣泛應用在ReactiveCocoa對於屬性變化的影響動作中。然而KVO用起來即不簡單也不開心:它的API有很多過度設計的參數,以及缺乏方便的block方式調用。
RAC 與 Bindings
Bindings也是黑魔法。
雖然對OS X控制的要點就是Bindings,但是它的意義在近年來越來越沒那么重要了,因為焦點已經移動到了iOS和UIKit這些Bindings不支持的東西身上。Bindings替代了大量的模版膠水代碼,允許在Interface Builder中完成編碼,但嚴格上說還是比較有局限性的,並且_無法_debug。RAC提供了一種簡潔易懂、擴展性強的以代碼為基礎的API來運行在iOS上,目標就是取代所有在OS X能用Bindings實現的神奇功能。
Objective-C在C的核心上吸收了Smalltalk的思想建立而成,但哲學理念上已經超越了它原本來源的血統。
@protocol
是對C++多重繼承的拒絕,順應抽象數據的類型范式是對Java Interface
的吸收。Objective-C 2.0引入了@property / @synthesize
則靈感來自C#的 get; set;
方法對getter和setter的速記(就語法上來說,這也是NeXTSETP強硬路線堅持者經常辯論的一點)。Block給這門語言帶來了函數式編程的好處,可以使用Grand Central Dispatch——來自Fortran / C / C++ standard OpenMP思想而成的基於隊列的並發API。下標和對象字面量都是像Ruby、Javascript這樣的腳本語言的標准特性,如今也由一個Clang插件被帶入了Objective-C的世界里。
ReactiveCocoa則給Objective-C帶來了函數響應式編程的健康葯劑。它本身也是受C#的Rx library、Clojure和Elm的影響發展而成。
好的點子會傳染。ReactiveCocoa就是一種警示,提醒人們好的點子也可以從看似不太可能的地方傳播過來,這樣的新鮮思想對解決類似的問題也會有完全不同的方法呢。
下一節,我們來一起看一下如何使用RAC來完成異步的功能.