ReactiveCocoa與Functional Reactive Programming


 

轉自  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提供了一種信號機制來實現這樣的效果,通過信號來記錄值的變化。信號可以被疊加、分割或合並。通過對信號的組合,就不需要去監聽某個值或事件。

FRP demo

這在重交互的應用里是非常有用的。以注冊為例:

register demo

提交按鈕的狀態要跟輸入框的狀態綁定,比如必選的輸入框沒有填完時,提交按鈕是灰色的,也就是不可點;如果提交按鈕不可點,那么文字變成灰色,不然變成藍色;如果正在提交,那么輸入框的文字顏色變成灰色,且不可點,不然變成默認色且可點;如果注冊成功就在狀態欄顯示成功信息,不然顯示錯誤信息,等等。

可以看到光是注冊頁就有這么多的聯動,在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在UITextFieldcategory中添加的,直接用即可。

combine signal

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的使用情景和用法,更多的例子可以到項目主頁中查看。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM