轉自 http://blog.leezhong.com/ios/2013/06/19/frp-reactivecocoa.html
Functional Reactive Programming(以下簡稱FRP)是一種響應變化的編程范式。先來看一小段代碼
a = 2
b = 2
c = a + b // c is 4
b = 3
// now what is the value of c?
如果使用FRP,c
的值將會隨着b
的值改變而改變,所以叫做「響應式編程」。比較直觀的例子就是Excel,當改變某一個單元格的內容時,該單元格相關的計算結果也會隨之改變。
FRP提供了一種信號機制來實現這樣的效果,通過信號來記錄值的變化。信號可以被疊加、分割或合並。通過對信號的組合,就不需要去監聽某個值或事件。
這在重交互的應用里是非常有用的。以注冊為例:
提交按鈕的狀態要跟輸入框的狀態綁定,比如必選的輸入框沒有填完時,提交按鈕是灰色的,也就是不可點;如果提交按鈕不可點,那么文字變成灰色,不然變成藍色;如果正在提交,那么輸入框的文字顏色變成灰色,且不可點,不然變成默認色且可點;如果注冊成功就在狀態欄顯示成功信息,不然顯示錯誤信息,等等。
可以看到光是注冊頁就有這么多的聯動,在javascript中可以采用事件監聽來處理,iOS中更多的是delegate模式,本質上都是事件的分發和響應。這種做法的缺點是不夠直觀,尤其在邏輯比較復雜的情況下。這也是為什么盡管nodejs很高效,但由於javascript的callback style和異步模式不符合正常的編程習慣,讓很多人望而卻步。
使用FRP主要有兩個好處:直觀和靈活。直觀的代碼容易編寫、閱讀和維護,靈活的特性便於應對變態的需求。
ReactiveCocoa
ReactiveCocoa是github去年開源的一個項目,是在iOS平台上對FRP的實現。FRP的核心是信號,信號在ReactiveCocoa(以下簡稱RAC)中是通過RACSignal
來表示的,信號是數據流,可以被綁定和傳遞。
可以把信號想象成水龍頭,只不過里面不是水,而是玻璃球(value),直徑跟水管的內徑一樣,這樣就能保證玻璃球是依次排列,不會出現並排的情況(數據都是線性處理的,不會出現並發情況)。水龍頭的開關默認是關的,除非有了接收方(subscriber),才會打開。這樣只要有新的玻璃球進來,就會自動傳送給接收方。可以在水龍頭上加一個過濾嘴(filter),不符合的不讓通過,也可以加一個改動裝置,把球改變成符合自己的需求(map)。也可以把多個水龍頭合並成一個新的水龍頭(combineLatest:reduce:),這樣只要其中的一個水龍頭有玻璃球出來,這個新合並的水龍頭就會得到這個球。
下面通過一個簡單的demo來演示這個模型。
假如對象的某個屬性想綁定某個消息,可以使用RAC
這個宏,相當於給玻璃球套了一個水龍頭。
RAC(self.submitButton.enabled) = [RACSignal combineLatest:@[self.usernameField.rac_textSignal, self.passwordField.rac_textSignal] reduce:^id(NSString *userName, NSString *password) {
return @(userName.length >= 6 && password.length >= 6);
}];
這樣,如果用戶名和密碼框的長度都超過6,提交按鈕就enable了。反之,如果沒符合要求,就會處於非開啟狀態。
可以看到usernameField
有了一個新的屬性rac_textSignal
,這是RAC在UITextField
category中添加的,直接用即可。
RAC的大統一
RAC統一了對KVO、UI Event、Network request、Async work的處理,因為它們本質上都是值的變化(Values over time)。
KVO
RAC可以用來監測屬性的改變,這點跟KVO很像,不過使用了block,而不是-observeValueForKeyPath:ofObject:change:context:
[RACAble(self.username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
使用起來是不是比KVO舒服多了。比KVO更加強大的是信號可以被鏈起來(chain)
// 只有當名字以'j'開頭,才會被記錄
[[RACAble(self.username)
filter:^(NSString *newName) {
return [newName hasPrefix:@"j"];
}]
subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
UI Event
RAC還為系統UI提供了很多category,來方便消息的創建和傳遞,比如按鈕被點擊或文本框有改動等等,上面的例子中self.firstNameField.rac_textSignal
,在對應的文本框有改動時,會自動向數據流中添加新的數據,綁定該消息的其他消息就會收到新的數據,如果有subscriber的話,會自動觸發。
Network Request && Async work
這些可以通過自定義信號,也就是RACSubject
(繼承自RACSignal
,可以理解為自由度更高的signal)來搞定。比如一個異步網絡操作,可以返回一個subject,然后將這個subject綁定到一個subscriber或另一個信號。
- (void)doTest
{
RACSubject *subject = [self doRequest];
[subject subscribeNext:^(NSString *value){
NSLog(@"value:%@", value);
}];
}
- (RACSubject *)doRequest
{
RACSubject *subject = [RACSubject subject];
// 模擬2秒后得到請求內容
// 只觸發1次
// 盡管subscribeNext什么也沒做,但如果沒有的話map是不會執行的
// subscribeNext就是定義了一個接收體
[[[[RACSignal interval:2] take:1] map:^id(id _){
// the value is from url request
NSString *value = @"content fetched from web";
[subject sendNext:value];
return nil;
}] subscribeNext:^(id _){}];
return subject;
}
小結
簡單畫了下關系圖,羅列了些要點
上面只是大概說了一下RAC的使用情景和用法,更多的例子可以到項目主頁中查看。