天天看點

ReactiveCocoa Documents 翻譯(基于版本V2.5)1. 基本操作(Basic Operators)2 設計指南(Design Guidelines)3 與 Rx 的差異(Differences from Rx)4 架構概覽(Framework Overview)5. 記憶體管理(Memory Management)

1. 基本操作(Basic Operators)

描述 ReactiveCocoa 最常用的一些操作以及使用範例。 主要是如何運用 序列(sequences) 和 信号(signals) 的流操作。

用信号實作副作用(Performing side effects with signals)

  1. 訂閱(Subscription)
  2. 依賴注入(Injecting effects)

流的傳輸(Transforming streams)

  1. 映射(Mapping)
  2. 過濾(Filtering)

流的結合(Combining streams)

  1. 串聯(Concatenating)
  2. 壓縮(Flattening)
  3. 映射和壓縮(Mapping and flattening)

信号的結合(Combining signals)

  1. 排序(Sequencing)
  2. 合并(Merging)
  3. 結合最新值(Combining latest values)
  4. 切換(Switching)

1.1 用信号實作副作用(Performing side effects with signals)

譯者注:什麼是冷信号,什麼是熱信号?

  • 冷信号:被訂閱的時候才激活。信号預設是冷類型的。
  • 熱信号:傳回給調用者的時候已經被激活。

譯者注:什麼是 push-driven ? 什麼是 pull-driven ?

  • push-driven:在建立信号的時候,信号不會被立即指派,之後才會被指派(例如網絡請求回來的結果或者是任意的使用者輸入的結果)。
  • pull-driven:在建立信号的時候,序列中的值就會被确定下來,我們可以從流中一個個的查詢值。

大多數信号(signals)初始化是冷(cold)信号,這意味着它們在被訂閱(Subscription)之前不會做任何工作。

訂閱之後,信号或者它的訂閱者能夠完成邊界效應,比如輸出日志到控制台,做一個網絡請求,修改使用者界面等。

副作用也可以被注入到一個信号,這樣的副作用不會立刻完成,但是會被稍後的每一個訂閱者觸發副作用。 

副作用: 當調用函數時,除了傳回函數值之外,還對主調用函數産生附加影響,這就叫函數的副作用。 ReactiveCocoa 的函數參數是 In/Out 作用的參數,即函數可能改變參數裡面的的内容,把一些資訊通過輸入參數,夾帶到外界。 這種情況嚴格來說也是副作用,是非純函數。我們所讨論的函數式反應式程式設計中的函數式程式設計屬于非純函數,它是有副作用的。

1.1.1 訂閱(Subscription)

-subscribe...

 方法給你機會通路信号中的目前或者将來的值:

RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

// 輸出: A B C D E F G H I
[letters subscribeNext:^(NSString *x) {
    NSLog(@"%@", x);
}];
           

對于冷信号來說,副作用會在 每一次訂閱 時發生:

__block unsigned subscriptions = 0;

RACSignal *loggingSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    subscriptions++;
    [subscriber sendCompleted];
    return nil;
}];

// 輸出:
// subscription 1
[loggingSignal subscribeCompleted:^{
    NSLog(@"subscription %u", subscriptions);
}];

// 輸出:
// subscription 2
[loggingSignal subscribeCompleted:^{
    NSLog(@"subscription %u", subscriptions);
}];
           

行為可以被 

connection

(後面會講到connection) 改變.

1.1.2 注入影響(Injecting effects)

do...

方法給信号添加副作用而不需要實際訂閱信号:

__block unsigned subscriptions = 0;

RACSignal *loggingSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    subscriptions++;
    [subscriber sendCompleted];
    return nil;
}];

// 沒有任何輸出
loggingSignal = [loggingSignal doCompleted:^{
    NSLog(@"about to complete subscription %u", subscriptions);
}];

// 輸出:
// about to complete subscription 1
// subscription 1
[loggingSignal subscribeCompleted:^{
    NSLog(@"subscription %u", subscriptions);
}];
           

1.2 流的轉換(Transforming streams)

下面的操作将流轉換為一個新的流。

1.2.1 映射(Mapping)

-map...

 方法被用來傳遞一個值給流,然後用該值建立一個新的流。

RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;

// Contains: AA BB CC DD EE FF GG HH II
RACSequence *mapped = [letters map:^(NSString *value) {
    return [value stringByAppendingString:value];
}];
           

1.2.2 過濾(Filtering)

-filter...

 方法用一個 block 測試每一個值,使得結果流中隻包含測試通過的内容。

RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: 2 4 6 8
RACSequence *filtered = [numbers filter:^ BOOL (NSString *value) {
    return (value.intValue % 2) == 0;
}];
           

1.3 流的合并(Combining streams)

下面的操作合并多個流到一個單一的新流。

1.3.1 串聯(Concatenating)

-concat:

方法追加一個流的值到另外一個流後面。

RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *concatenated = [letters concat:numbers];
           

1.3.2 扁平(Flattening,這個真不知道怎麼翻譯)

-flatten

 操作适用于基于流的流,合并他們的值到一個新的流中。 

序列是串聯(concatenated)的

RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *sequenceOfSequences = @[ letters, numbers ].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *flattened = [sequenceOfSequences flatten];
           

信号是合并(merged)的:

RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    [subscriber sendNext:letters];
    [subscriber sendNext:numbers];
    [subscriber sendCompleted];
    return nil;
}];

RACSignal *flattened = [signalOfSignals flatten];

// Outputs: A 1 B C 2
[flattened subscribeNext:^(NSString *x) {
    NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];
           

1.3.3 映射和扁平(Mapping and flattening)

flattening

 本身并不有趣,但弄懂它的 

-flattenMap

 方法是如何工作的很重要。 

-flattenMap:

 用來傳遞流的每一個值到 一個新的流 。然後,所有傳回的流會被扁平到一個新的流。 也就是說,它相當于 

-flatten

 操作後再 

-map:

 操作。

可用來擴充或編輯序列:

RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
RACSequence *extended = [numbers flattenMap:^(NSString *num) {
    return @[ num, num ].rac_sequence;
}];

// Contains: 1_ 3_ 5_ 7_ 9_
RACSequence *edited = [numbers flattenMap:^(NSString *num) {
    if (num.intValue % 2 == 0) {
        return [RACSequence empty];
    } else {
        NSString *newNum = [num stringByAppendingString:@"_"];
        return [RACSequence return:newNum]; 
    }
}];
           

或者建立多個信号自動合并:

RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

[[letters
    flattenMap:^(NSString *letter) {
        return [database saveEntriesForLetter:letter];
    }]
    subscribeCompleted:^{
        NSLog(@"All database entries saved successfully.");
    }];
           

1.4 信号組合(Combining signals)

下面的操作組合多個信号到一個新信号。

1.4.1 序列化(Sequencing)

-then:

 啟動原始的信号,等待它完成,然後隻轉發新信号中的值。

RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

// The new signal only contains: 1 2 3 4 5 6 7 8 9
//
// But when subscribed to, it also outputs: A B C D E F G H I
RACSignal *sequenced = [[letters
    doNext:^(NSString *letter) {
        NSLog(@"%@", letter);
    }]
    then:^{
        return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;
    }];
           

這在某些情況下很有用,比如執行一個信号的所有副作用,然後開始另外一個信号,并且隻傳回第二個信号的值。

1.4.2 合并(Merging)

+merge:

 方法會盡可能快的從很多信号中轉發值到一個流中,在值到達的時候。

RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *merged = [RACSignal merge:@[ letters, numbers ]];

// Outputs: A 1 B C 2
[merged subscribeNext:^(NSString *x) {
    NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];
           

1.4.3 組合最新值(Combining latest values)

+ combineLatest:

 和 

+combineLatest:reduce:

 方法會觀察多個信号的改變,然後從所有信号發送最新的值:

RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *combined = [RACSignal
    combineLatest:@[ letters, numbers ]
    reduce:^(NSString *letter, NSString *number) {
        return [letter stringByAppendingString:number];
    }];

// Outputs: B1 B2 C2 C3
[combined subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[numbers sendNext:@"2"];
[letters sendNext:@"C"];
[numbers sendNext:@"3"];
           

注意結合信号會隻發送第一個值,當所有輸入被發送至少一個的時候。上面的例子中,

@"A"

 不會被轉發因為 

number

 沒有被發送一個值。

1.4.4 切換(Switching)

-switchToLatest

 操作适用于基于信号的信号,并且總是從最新信号傳遞值。

RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSubject *signalOfSignals = [RACSubject subject];

RACSignal *switched = [signalOfSignals switchToLatest];

// Outputs: A B 1 D
[switched subscribeNext:^(NSString *x) {
    NSLog(@"%@", x);
}];

[signalOfSignals sendNext:letters];
[letters sendNext:@"A"];
[letters sendNext:@"B"];

[signalOfSignals sendNext:numbers];
[letters sendNext:@"C"];
[numbers sendNext:@"1"];

[signalOfSignals sendNext:letters];
[numbers sendNext:@"2"];
[letters sendNext:@"D"];
           

2 設計指南(Design Guidelines)

本文檔包含如何在工程中使用 ReactiveCocoa 的設計指南。本章的内容重度參考了 Rx Design Guidelines。

本文假設讀者熟悉 ReactiveCocoa 的基本功能。

Framework Overview

 是開始了解 RAC 的更好的資源。

RACSequence

的約定

  1. 運算預設是懶執行模式。
  2. 運算會阻塞調用者。
  3. 副作用隻發生一次。

RACSignal

的約定

  1. 信号事件是串行的。
  2. 訂閱都是發生在排程的時候。
  3. 錯誤會被立即傳送出來。
  4. 副作用發生在每次訂閱。
  5. 訂閱會自動部署完成和錯誤。
  6. Disposal取消正在進行的工作并且清除資源。

最佳實踐

  1. 為傳回信号的方法或屬性使用描述性的聲明。
  2. 始終縮進流操作。
  3. 流的所有值都使用相同的類型。
  4. 不要retain流過長時間。
  5. 隻處理需要數量的流。
  6. 分發信号事件到一個已知的排程。
  7. 較少的場合需要切換排程者。
  8. 明确信号的副作用。
  9. 通過multicasting共享信号的副作用。
  10. 通過指定的流的名字來調試。
  11. 避免明确的訂閱(subscriptions)和釋放(disposal)。
  12. 盡可能避免使用subjects。

完成新operators

  1. 優先使用基于 RACStream 方法。
  2. 盡可能組合已存在的operators。
  3. 避免引入并發。
  4. 在dispasable中取消任務和清理所有資源。
  5. 在operator中不要阻塞。
  6. 深度遞歸要避免棧溢出。

2.1 RACSequence的約定(The RACSequence contract)

RACSequence

 是 pull-driven 流。序列行為類似内置集合,但有些不一樣的地方。

2.1.1 (運算預設是懶執行模式)Evaluation occurs lazily by default

序列運算預設是懶執行模式,如下面的序列:

NSArray *strings = @[ @"A", @"B", @"C" ];
RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
    return [str stringByAppendingString:@"_"];
}];
           

沒有字元串會被實際追加直到序列真正需要的時候。 通路 

sequence.head

 會完成 

A_

 的拼接,通路 

sequence.tail.head

 會完成 

B_

 的拼接,等等。

這通常能避免不必要的工作(因為不需要的值不會被計算),但意味着序列是要處理的。

一旦被計算,序列中的值就被存儲不會被重新計算。通路 

sequence.head

 多次隻會做一次字元串的拼接工作。

如果懶式運算模式不可取 - 例如,因為記憶體有限的時候,較少使用記憶體更重要 - eagerSequence 屬性可能被強制轉為饑渴模式。

2.1.2 運算會阻塞調用者(Evaluation blocks the caller)

不管序列是懶模式還是饑渴模式,運算序列的任何部分都會阻塞調用者線程直到任務完成。阻塞是必須的因為值必須從序列中同步傳回。

如果運算序列序列的代價大到可能阻塞線程很明顯的時間,考慮用 

-signalWithScheduler:

 建立一個信号然後用它來替代序列。

2.1.3 副作用隻發生一次(Side effects occur only once)

當傳遞給序列操作的 block 引發了副作用,要明白副作用對每個值隻會發生一次,就是在值被運算的時候。

NSArray *strings = @[ @"A", @"B", @"C" ];
RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
    NSLog(@"%@", str);
    return [str stringByAppendingString:@"_"];
}];

// Logs "A" during this call.
NSString *concatA = sequence.head;

// Logs "B" during this call.
NSString *concatB = sequence.tail.head;

// Does not log anything.
NSString *concatB2 = sequence.tail.head;

RACSequence *derivedSequence = [sequence map:^(NSString *str) {
    return [@"_" stringByAppendingString:str];
}];

// Still does not log anything, because "B_" was already evaluated, and the log
// statement associated with it will never be re-executed.
NSString *concatB3 = derivedSequence.tail.head;
           

2.2 RACSignal 約定(The RACSignal contract)

RACSignal

 是 push-driven 流,專注于通過 subscriptions 分發異步事件。 關于信号和訂閱的更多内容,參見 

Framework Overview

2.2.1 信号事件是串行的(Signal events are serialized)

信号可以在任何線程中分發事件。連續的事件甚至被允許分發到不同的線程或者排程者,除非顯示的指定了分發到特定的排程者。

然而,RAC 不會有兩個信号并發到達。一個事件被處理時,不會有另外的事件被分發。其他事件的發送會被強制等待直到目前事件被處理完成。

特别注意,這意味着傳遞給 

-subscribeNext:error:competed:

 的 block 之間不需要考慮同步,因為他們永遠不會被同時調用。

2.2.2 訂閱總會在排程的時候發生(Subscription will always occur on a scheduler)

要保證 

+createSingnal

 和 

-subscribe:

 的行為一緻,每一個 

RACSignal

 必須確定在合法的排程者上訂閱。

如果的訂閱者的線程已經有一個 

+currentScheduler

 ,排程會立刻發生;否則,會在背景排程的時候立刻發生。 注意主線程總是與 

—mainThreadScheduler

 關聯,是以主線程的訂閱總是立刻發生。

參見文檔 

-subscribe:

 擷取更多資訊。

2.2.3 錯誤會立即傳送出來(Errors are propagated immediately)

在 RAC中,

error

 事件有特别的語義。當錯誤被發送給信号,會立即轉發給所有依賴的信号,引發整個依賴鍊的終止。

Operators

 的主要目的是改變錯誤處理行為,但像 

-catch:

-catchTo:

, 或者 

-materialize

 明顯是不符合此規則的。

2.2.4 副作用發生在每次訂閱時(Side effects occur for each subscription)

對 

RACSignal

 的每一個新的訂閱都會觸發信号的副作用。這意味着任何副作用發生的次數和信号訂閱的次數一樣多。

考慮如下代碼:

__block int aNumber = 0;

// Signal that will have the side effect of incrementing `aNumber` block 
// variable for each subscription before sending it.
RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    aNumber++;
    [subscriber sendNext:@(aNumber)];
    [subscriber sendCompleted];
    return nil;
}];

// This will print "subscriber one: 1"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber one: %@", x);
}];

// This will print "subscriber two: 2"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber two: %@", x);
}];
           

副作用會在每次訂閱的時候重複發生。同樣适用于 

stream

 和 

signal

 操作。

__block int missilesToLaunch = 0;

// Signal that will have the side effect of changing `missilesToLaunch` on
// subscription.
RACSignal *processedSignal = [[RACSignal
    return:@"missiles"]
    map:^(id x) {
        missilesToLaunch++;
        return [NSString stringWithFormat:@"will launch %d %@", missilesToLaunch, x];
    }];

// This will print "First will launch 1 missiles"
[processedSignal subscribeNext:^(id x) {
    NSLog(@"First %@", x);
}];

// This will print "Second will launch 2 missiles"
[processedSignal subscribeNext:^(id x) {
    NSLog(@"Second %@", x);
}];
           

要阻止上述行為,在多次訂閱一個信号時隻執行它的副作用一次,可以用信号的多點傳播功能 

multicasted

2.2.5 訂閱會在完成和錯誤的時候自動釋放(Subscriptions are automatically disposed upon completion or error)

當一個 

subscriber

 被發送給 

completed

 或者 

error

 事件,相關的訂閱會自動被釋放。這種行為通常無需手動去配置訂閱。

參見文檔 

Memory Management

 擷取 

signal

 的更多資訊。

2.2.6 Disposal取消正在進行的工作和清理資源(Disposal cancels in-progress work and cleans up resources)

訂閱被釋放的時候,不管手動或自動,任何正在處理或與訂閱相關的工作會盡快被取消,訂閱相關的資源會被釋放。

2.3 Best practices

下面的建議有助于保證基于 RAC 的代碼可預測,可了解和高效。

然而,僅僅隻是指導。判斷是否遵循了建議的标準是下面的代碼片段。

2.3.1 為傳回信号的屬性和方法使用描述性的聲明(Use descriptive declarations for methods and properties that return a signal)

方法或屬性如果傳回 

RACSignal

 類型,會很難看懂信号的語義。

有三個關鍵問題必須在聲明中表達清楚:

  1. 信号是 熱 (傳回給調用者的時候已經被激活)信号還是 冷 (被訂閱的時候才激活)信号。
  2. 信号包含0個,1個,還是多個值?
  3. 信号是否有副作用?

沒有副作用的熱信号 應該典型的用屬性來代替方法。使用屬性意味着在訂閱信号的時間之前不需要初始化,并且額外的訂閱不會改變語義。 信号屬性應該以事件命名(例如 

textChanged

)。

沒有副作用的冷信号 應該從名詞命名的方法中傳回(例如:

-currentText

)。 這樣的方法聲明意味着信号不會被該方法保留,暗示着在訂閱的時候任務已經完成了。 如果信号發送多個值,名詞應該用複數(例如: 

-currentModels

)。

帶副作用的信号 應該被動詞命名的方法傳回(例如:

-logIn

)。 動詞意味着該方法不是幂等的(幂等:意味着多次執行操作的結果和第一次執行的相同。應該就是函數的可重入性), 調用者必須小心隻在副作用需要的時候才調用它。如果信号會發送一個或多個值,應該包含一個期望的名詞 (例如:

-loadConfigration

, 

-fecthLatestEvents

)。

2.3.2 始終縮進流操作(Indent stream operations consistently)

如果沒有合适的格式化,流代碼和容易變得密集和混亂。使用縮進能夠清晰的看出來鍊式流操作的開始和結束。

調用流的簡單的方法時,不需要要額外的縮進。:

RACStream *result = [stream startWith:@0];

RACStream *result2 = [stream map:^(NSNumber *value) {
    return @(value.integerValue + 1);
}];
           

如果傳輸同一個流多次,確定每一個步驟都是對齊的。 複雜的操作比如 

+zip:reduce:

 或 

+combineLatest:reduce:

 可以拆分成多行提高可讀性。

RACStream *result = [[[RACStream
    zip:@[ firstStream, secondStream ]
    reduce:^(NSNumber *first, NSNumber *second) {
        return @(first.integerValue + second.integerValue);
    }]
    filter:^ BOOL (NSNumber *value) {
        return value.integerValue >= 0;
    }]
    map:^(NSNumber *value) {
        return @(value.integerValue + 1);
    }];
           

當然,帶block參數的嵌套的流應該跟block一起自然縮進:

[[signal
    then:^{
        @strongify(self);

        return [[self
            doSomethingElse]
            catch:^(NSError *error) {
                @strongify(self);
                [self presentError:error];

                return [RACSignal empty];
            }];
    }]
    subscribeCompleted:^{
        NSLog(@"All done.");
    }];
           

2.3.3 對流的所有的值使用相同的類型(Use the same type for all the values of a stream)

RACStream

 (包括它的擴充,

RACSignal

 和 

RACSequence

)允許流由異質對象(即對象的類型不一緻)組成,就像 Cocoa 集合那樣。

然而,在流中使用不同的類型使得操作複雜,還會給流的使用者帶來額外的負擔,使用者應該隻關心如何調用支援的方法。

任何可能的時候,流都應該隻包含相同類型的對象。

2.3.4 避免長時間持有流(Avoid retaining streams for too long)

保留 

RACStream

 超過必要的時間會引發關于保留的依賴問題,如記憶體使用過高等。

RACSequence

 應該隻被保留序列的 

head

 需要被保留的那麼長時間。如果 head 不再被使用,保留節點的 tail 代替節點本身。 

參見 

Memory Management

 指引擷取關于對象生命周期的更多資訊。

2.3.5 隻處理需要數量的流(Process only as much of a stream as needed)

讓流或者 

RACSignal

 的訂閱保持不必要的活躍狀态會導緻CPU使用增長。

如果流中隻有特定數量的值需要被用到,

-take:

 操作可以用來傳回這些值,然後傳回值之後立即自動結束流。

類似 

-take:

 和 

-takeUntil

 等操作能自動釋放棧。如果其餘的值不再需要,任何依賴也會結束,這可以顯著的減少潛在的開銷。

2.3.6 分發信号事件到一個已知的排程(Deliver signal events onto a known scheduler)

當信号被一個方法傳回,或者被信号組合,很難搞清楚是在哪個線程上事件被分發。 盡管事件被確定是串行的,但有時候需要更嚴格的情形,比如 UI 的重新整理必須在主線程。

無論何時保證事件是串行的都很重要,

-deliverOn:

 操作應該被用來強制信号事件到達一個明确的 

RACScheduler

2.3.7 (較少的場合需要切換排程者)Switch schedulers in as few places as possible

在滿足上面的情況下,事件還應該在必要的時候分發到明确的 

scheduler

。切換排程這會引入不必要的時延和 CPU 負擔。

通常,使用 

-deliverOn:

 應該被限制在信号鍊的末端。例如,在訂閱之前,或者在被綁定到一個屬性之前。

2.3.8 明确信号的副作用(Make the side effects of a signal explicit)

RACSignal

 的副作用應該盡可能避免,因為訂閱可能出現行為副作用異常。

然而,有時候信号時間發生時,副作用是有用的。 盡管大多數 

RACStream

 和 

RACSignal

 操作接受任意的 block (有副作用的), 使用 

-doNext:

-doError:

-doCpmpleted:

 能更明确和自解釋副作用的發生。

NSMutableArray *nexts = [NSMutableArray array];
__block NSError *receivedError = nil;
__block BOOL success = NO;

RACSignal *bookkeepingSignal = [[[valueSignal
    doNext:^(id x) {
        [nexts addObject:x];
    }]
    doError:^(NSError *error) {
        receivedError = error;
    }]
    doCompleted:^{
        success = YES;
    }];

RAC(self, value) = bookkeepingSignal;
           

2.3.9 用多點傳播共享信号副作用(Share the side effects of a signal by multicasting)

預設情況下,副作用在每次訂閱的時候發生,但在某些特定的情況下副作用應夠隻發生一次--例如, 一個網絡請求很明顯不應該在添加新的訂閱的時候重複調用。

RACSignal

 的 

-publish

 和 

-multicast:

 操作允許一個單一的訂閱通過使用 

RACMulticastConnection

 共享給多個訂閱者。

// This signal starts a new request on each subscription.
RACSignal *networkRequest = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    AFHTTPRequestOperation *operation = [client
        HTTPRequestOperationWithRequest:request
        success:^(AFHTTPRequestOperation *operation, id response) {
            [subscriber sendNext:response];
            [subscriber sendCompleted];
        }
        failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            [subscriber sendError:error];
        }];

    [client enqueueHTTPRequestOperation:operation];
    return [RACDisposable disposableWithBlock:^{
        [operation cancel];
    }];
}];

// Starts a single request, no matter how many subscriptions `connection.signal`
// gets. This is equivalent to the -replay operator, or similar to
// +startEagerlyWithScheduler:block:.
RACMulticastConnection *connection = [networkRequest multicast:[RACReplaySubject subject]];
[connection connect];

[connection.signal subscribeNext:^(id response) {
    NSLog(@"subscriber one: %@", response);
}];

[connection.signal subscribeNext:^(id response) {
    NSLog(@"subscriber two: %@", response);
}];
           

2.3.10 通過給定的名字調試流(Debug streams by giving them names)

每一個 

RACStream

 有一個 

name

 屬性用來協助調試。 流的 

description

 包含流的名稱,并且 RAC 所有的操作都會添加這個名稱。從名稱可以很友善的辨別出一個流。

例如如下代碼片段:

RACSignal *signal = [[[RACObserve(self, username) 
    distinctUntilChanged] 
    take:3] 
    filter:^(NSString *newUsername) {
        return [newUsername isEqualToString:@"joshaber"];
    }];

NSLog(@"%@", signal);
           

上面的代碼會記錄一個類似 

[[[RACObserve(self, username)] -distinctUntilChanged] -take: 3] -filter:

 的名稱。

名稱也可以通過 

-setNameWithFormat:

 手工添加。

RACSignal

 也提供 

-logNext

-logError

-logCompleted

 和 

-logAll

 方法, 這些方法在事件發生時自動記錄信号事件,包括信号名稱和消息。這可以為實時觀察信号提供便利。

2.3.11 避免明确的訂閱和釋放(Avoid explicit subscriptions and disposal)

盡管 

-subscribeNext:error:completed:

 和它的變體是處理信号的最基本的方式,但它們使用較少的聲明導緻代碼複雜, 推薦使用副作用,盡量複用潛在的内建功能。

同樣的,明确的使用 

RACDisposable

 類能快速導緻老鼠窩一樣(啥意思,一團糟的意思嗎?)的資源管理和代碼清除。

下面是幾乎總是該遵循的進階模式,用來替換手動訂閱和釋放:

  • RAC

     或 

    RACChannelTo

     宏能夠用來綁定信号到一個屬性,用來代替在改變發生時手動更新的機制。
  • -rac_liftSelector:withSignals:

     方法能夠用來在信号觸發時自動調用一個 selector 。
  • -takeUntil:

     之類的操作在時間發生時能夠用來自動釋放訂閱(例如 UI 的‘取消’按鈕被按下)。

通常,相比在訂閱的回調中完成相同功能,使用 

stream

 和 

signal

 内建的操作隻需更簡單更少出錯的代碼。

2.3.12 盡可能避免使用 subjects(Avoid using subjects when possible)

Subjects

 是信号用來橋接指令式代碼和現實世界的一個強有力的工具。但是對于可變 RAC 來說,他們的過度使用很快會導緻代碼複雜。

因為 Subjects 可以在任何地方任何時間使用,是以 subjects 經常打破 stream 的線性處理,導緻邏輯複雜。 Subjects 也不支援嚴格的 disposal,嚴格的 disposal 會引入不必要的任務。

Subjects 能夠被 ReactiveCocoa 的下列其他模式替換:

  • 考慮用 

    +createSignal

     block 生成值 來代替 提供初始化值到一個 subject 中。
  • 考慮用 

    +combineLatest:

     或 

    +zip:

     等操作合并多個信号的輸出 來代替 分發中間結果給 subject。
  • 考慮用 

    multicast

     多點傳播基本的信号 來代替 使用subjects共享多個訂閱的結果。
  • 考慮用 

    command

     或 

    -rac_signalForSelector:

     來代替 實作多個動作方法來實作對 subject 的簡單控制。

當 subject 必須使用時,他們幾乎總是被使用在信号鍊的基本輸入,而不是在信号鍊中間使用。

2.4 實作一個新的操作(Implementing new operators)

RAC 為 

stream

 和 

signal

 提供了大量内建操作,能夠滿足大部分應用場景;然而,RAC 不是一個封閉的系統。 ReactiveCocoa 考慮了為了特殊的用途實作一些額外的操作。

實作新的操作需要特别注意一些細節和簡單化操作,避免在調用的代碼中引入bug。

下面的指南包括一些通用的原則能夠幫助編寫符合預期的 API:

2.4.1 優先使用基于 RACStream 的方法(Prefer building on RACStream methods)

RACStream

 提供的接口比 

RACSequence

 和 

RACSignal

 更簡單,而且所有的 stream 操作也适用于 sequence 和 signal。

基于這個原因,無論何時新操作都應該基于 

RACStream

 的方法實作。 至少需要 

RACStream

 類的 

-bind:

-zipWith:

, 和 

-concat:

 方法,這些方法就已經很強大了, 不需要添加其他任何功能就可以完成很多任務。

如果一個新的 

RACSignal

 操作需要處理 

error

 和 

completed

 事件, 考慮使用 

-materialize

 方法給 stream 引入事件。 所有 materialized 的信号事件都能夠被流操作修改,這能幫助最小化使用非 stream 的操作(???這句話沒整明白)。

2.4.2 盡可能組合已存在的操作(Compose existing operators when possible)

RAC 經過了深思熟慮,通過了合法性測試,也在很多項目中被使用。重新改寫操作的代碼可能不會有很好的健壯性,或者不能處理一些内建操作已經考慮到的特殊情況。

為了最小的重複代碼和盡可能少的引入 bug,在自定義的操作實作中,盡可能使用已提供的功能。通常隻有很少的代碼需要重寫。

2.4.3 避免引入并發(Avoid introducing concurrency)

在程式設計中,并發是非常容易引入 bug 的。為了盡可能的避免死鎖和競态,不應該引入并發。

調用者可以在一個明确的 

RACScheduler

 上訂閱和分發事件,RAC 提供強大的 

parallelize work

 方式而不會特别複雜。

2.4.4 在 disposable 中取消任務和清理所有資源(Cancel work and clean up all resources in a disposable)

使用 

+createSignal:

 方法建立一個信号時,它提供的 block 需要傳回一個 

RACDisposable

。 該 disposable 可以:

  • 可以友善的取消信号開始的任務。
  • 立即 dispose 到其他信号的訂閱,然後觸發取消和清理。
  • 釋放信号配置設定的記憶體和其它資源。

    這有助于實作 

    RACSignal 的約定

2.4.5 操作中不要阻塞(Do not block in an operator)

流操作應該立即傳回一個新流。任何操作需要完成的工作應該是新流的運算的一部分,而 不是 調用的流自身的一部分。

// WRONG!
- (RACSequence *)map:(id (^)(id))block {
    RACSequence *result = [RACSequence empty];
    for (id obj in self) {
        id mappedObj = block(obj);
        result = [result concat:[RACSequence return:mappedObj]];
    }

    return result;
}

// Right!
- (RACSequence *)map:(id (^)(id))block {
    return [self flattenMap:^(id obj) {
        id mappedObj = block(obj);
        return [RACSequence return:mappedObj];
    }];
}
           

如果要從流中傳回一個或多個值(例如 

first

),該規則可以忽略掉。

2.4.6 避免深度遞歸導緻棧溢出(Avoid stack overflow from deep recursion)

任何無限遞歸操作都需要使用 

RACScheduler

 的 

shceduleRecusiveBlock:

 方法。該方法會将遞歸轉換為疊代操作,防止棧溢出。

例如,下面是 

-repeat

 的一個錯誤的實作,肯定會導緻棧溢出和崩潰:

- (RACSignal *)repeat {
    return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];

        __block void (^resubscribe)(void) = ^{
            RACDisposable *disposable = [self subscribeNext:^(id x) {
                [subscriber sendNext:x];
            } error:^(NSError *error) {
                [subscriber sendError:error];
            } completed:^{
                resubscribe();
            }];

            [compoundDisposable addDisposable:disposable];
        };

        return compoundDisposable;
    }];
}
           

而下面的版本就會避免棧溢出:

- (RACSignal *)repeat {
    return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];

        RACScheduler *scheduler = RACScheduler.currentScheduler ?: [RACScheduler scheduler];
        RACDisposable *disposable = [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) {
            RACDisposable *disposable = [self subscribeNext:^(id x) {
                [subscriber sendNext:x];
            } error:^(NSError *error) {
                [subscriber sendError:error];
            } completed:^{
                reschedule();
            }];

            [compoundDisposable addDisposable:disposable];
        }];

        [compoundDisposable addDisposable:disposable];
        return compoundDisposable;
    }];
}
           

3 與 Rx 的差異(Differences from Rx)

ReactiveCocoa (RAC) 深受 .NET 的 

Reactive Extensions

 (Rx)影響,但不是直接的移植。 RAC 的一些原則和接口對于已經熟悉 Rx 的開發者來說都會迷惑,但還是表達了相同的算法。

一些不同之處,像方法和類的命名,是為了符合 Cocoa 現有的風格。 還有一些是在 Rx 上的改進,或者從其他函數式響應式程式設計範式中借鑒的(例如 Elm 程式設計語言)。

下面,嘗試講解 RAC 和 Rx 的不同。

3.1 接口(Interfaces)

EAC 不提供類似 .NET 中的 

IEnumerable

 和 

IObserver

 接口。RAC 中主要用三個類來代替:

  • RACStream 實作了基本流操作的抽象類。 實作了常用的 LINQ(Language Integrated Query,C# 術語,用于友善的執行一些增删查改等)操作。
  • RACSignal 是 

    RACStream

     的一個具體的子類,實作了 pish-driven 流,跟 

    IObserver

     很像。 在該類或 

    RACSignal+Operations

     類别中可以找到基于時間的操作,或者處理 

    completed

     和 

    error

     事件的方法。
  • RACSequence 也是 

    RACStream

     的一個具體的子類實作了 pull-driven 流,跟 

    IEnumerable

     很像。

3.2 流操作的名稱(Names of Stream Operations)

RAC 通常使用 LINQ 風格命名流方法。大多數異常處理深受 Haskell 和 Elm 的影響。

注意如下不同:

  • 用 

    -map:

     代替 

    Select

  • 用 

    -filter:

     代替 

    Where

  • 用 

    -flatten

     代替 

    Merge

  • 用 

    -flattenMap:

     代替 

    SelectMany

LINQ 操作在 RAC 中名稱有變化(但行為或多或少相同),在文檔中有記載,例如:

// Maps `block` across the values in the receiver.
//
// This corresponds to the `Select` method in Rx.
//
// Returns a new stream with the mapped values.
- (instancetype)map:(id (^)(id value))block;
           

4 架構概覽(Framework Overview)

本文包含 ReactiveCocoa 架構不同元件的一些高層次的描述,并且嘗試說明他們如何在一起工作,然後分别承擔什麼職責。 這意味着要先了解本文,然後才學習其他新子產品和其他特定文檔。

範例和如何使用 RAC,參見 

README

 或 

Design Guidelines

4.1 流(Streams)

RACStream

 抽象類用來描述流,是存儲對象值得所有序列。

值可以立即獲得,也可以在未來某個時間獲得,但必須是按順序擷取。在沒有計算或等到第一個值之前是無法擷取第二個值的。

流是遊離的(monads,遊牧的)?還允許基于一些基本操作建構複雜的操作(特别是 

-bind:

)。 RACStream 也從 

Haskell

 實作了 

Monoid

 和 

MonadZip

 類型類。??

RACStream

 自身并不是特别有用。大多數流被 

signal

 或 

sequences

 替代。

4.2 信号(Signals)

signal,由 

RACSignal

 表示,是 push-dirven 類型的流。

信号通常用來表示未來可能會被分發的資料。當任務完成或者資料被接受,值會被 發送 到信号,信号則推送他們到任何訂閱者。 使用者必須訂閱(subscribe)信号才能通路信号的值。

信号提供給訂閱者三種不同類型的事件:

  • next 事件提供流中的一個新值。

    RACStream

     方法隻操作這種類型的事件。
  • 不像 Cocoa 集合,它完全有效的信号是包括了 

    nil

    的.
  • error 事件表明在信号完成之前發生了錯誤。該事件會包含一個 

    NSError

     對象表明是什麼錯誤。
  • 錯誤必須特殊處理--錯誤不包含流中的值。
  • completed 事件表明信号成功完成了,并且完成之後不會再有值會被添加到流中。完成操作必須特殊處理--完成不包含流的值。

信号的生命周期由任意數量的 

next

 組成,跟随者 

錯誤(error)

 和 

完成(completed)

 (錯誤和完成二者隻存在一個,不會同時存在)。

4.2.1 訂閱(Subscription)

訂閱者 是唯一能從信号中等待或者能夠從信号中等待事件的主體。在 RAC 中,訂閱是任何遵從 

RACSubscriber

 協定的對象。

訂閱 在 

-subscribeNext:error:completed:

 或其他相應的方法中建立。 嚴格來說,大多數 

RACStream

 和 

RACSignal

 操作也能夠建立訂閱, 但這些中間狀态的訂閱通常隻是一個實作的細節(不是很明白)??

訂閱會保留他們訂閱的信号,不會自動釋放除非信号完成或者出錯。訂閱也能夠手動釋放。

4.2.2 Subjects

subject 用 

RACSubject

 來描述,是可以被手動控制的信号類型。

Subjects 可以認為是可變的信号,類似 

NSMutableArray

 和 

NSArray

 的關系。在橋接非 RAC 代碼和信号時很有用。

例如,處理應用邏輯的block,可以用發送事件到共享 subject 的 block 代替。 subject 随後傳回一個 

RACSignal

,隐藏 block 的實作細節。

某些 subject 提供額外的功能。

RACReplaySubject

 能夠用來為未來的訂閱者緩存事件, 就像網絡請求完成之前處理結果的東西都已準備好。

4.2.3 指令(Commands)

command 用 

RACCommand

 類來表示,為某些動作響應建立和訂閱信号。指令讓使用者與 app 互動實作副作用變的很容易。

通常行為觸發指令是由 UI 驅動的,例如一個按鈕被按下。 基于信号的指令能夠自動被禁用,這個禁用狀态能夠用 UI 中其他任何跟指令相關的控件來描述。

在 OS X 中,RAC 添加了一個 

rac_command

 屬性到 

NSButton

 用來自動設定按鈕的行為。

4.2.4 連接配接(Connections)

連接配接 用 

RACMulticastConnectiong

 類來描述,是可以在任意數量的訂閱者之間共享的訂閱。

信号預設是 cold ,意味着他們在每一次新訂閱者添加的時候開始工作。 這個行為通常是期望的,因為資料會為每個訂閱者重新整理和重新計算,但是如果信号有副作用或者任務代價很昂貴就會引入一些問題 (例如發送網絡請求)。

連接配接通過 

RACSignal

 類的 

-publish

 或者 

-multicast:

 方法建立, 并且不管連接配接有多少次訂閱,確定底層隻有一個訂閱被建立。一旦連接配接建立,連接配接的信号宣告是 hot 類型, 在這底層的訂閱會保留活動狀态直到連接配接的 所有 訂閱都被釋放。

4.3 序列(Sequences)

序列 用 

RACSequence

 類來描述,是 pull-driven 類型的流。

序列是一個集合,累世完成 

NSArray

 的功能。 跟 array 不一樣的是,序列的值預設是 lazily 的,隻在序列被需要的時候才會計算,提高了效率。 序列不能包含 

nil

序列類似 

閉包的序列

 或者  

Haskell

 中的 

List

 類型。

RAC 添加了 

-rac_qequence

 方法到大多數 Cocoa 的集合類,允許他們使用 

RACSequences

 來替代。

4.4 釋放(Disposables)

RACDisposable 類用來取消任務和資源清理。

Disposables 常用于信号的取消訂閱。 當訂閱被釋放,響應的訂閱者不會再收到信号的任何未來事件。 并且任何訂閱相關的工作(背景處理,網絡請求等)都會取消,因為結果不再需要了。

關于取消的更多資訊,參見 RAC 

Design Guidelines

4.5 排程(Schedulers)

排程 用 

RACScheduler

 類來描述,是一個串行執行隊列,信号在上面完成任務和分發結果。

排程類似 GCD 隊列,但排程支援取消,并且總是串行執行。 

+immediateScheduler

 異常時,排程不會提供同步執行。這可以避免死鎖,鼓勵使用信号操作代替 block。

RACScheduler

 有些方面也像 

NSOperatiaonQueue

,但排程不允許任務重新排序,也不支援依賴。

4.6 值類型(Value types)

RAC 提供少量雜項類來友善表達流中的值:

  • RACTuple 是個小的,固定帶笑傲的集合,能夠包含 

    nil

    (用 

    RACTupleNil

     表示)。經常用來表示多個流中合并的值。
  • RACUnit 是空值得單例。用來表示流中某些時候沒有存在意義的資料。
  • RACEvent 表示任意信号事件。主要被 

    RACSignal

     類的 

    -materialize

     方法使用。

5. 記憶體管理(Memory Management)

ReactiveCocoa 的記憶體管理非常複雜,但最終結果是 處理信号時,你并不需要保留他們。

如果架構要求你保留每一個信号,那這個架構使用起來就太笨重,特别是一次性的信号用于未來某個時候。 你不需要保留任何長時間活躍的信号到屬性中,然後確定用完之後再清除。這樣可沒勁了。

5.1 訂閱者(Subscribers)

無論去哪之前, 

subscribeNext:error:completed:

 (還有所有其所有變量)建立一個 隐式 的訂閱者使用給定的block。 任何這些 block 引用的對象會被保留為訂閱的一部分。就像其他對象,

self

 不會被保留除非有一個對它直接或間接的引用。

5.2 有限或短暫的信号(Finite or Short-Lived Signals)

RAC 記憶體管理最重要的原則是 訂閱在完成後錯誤時自動終止,訂閱者會被移除

例如,如果你的 view controller 中的代碼如下:

self.disposable = [signal subscribeCompleted:^{
    doSomethingPossiblyInvolving(self);
}];
           

… the memory management will look something like the following:

那麼記憶體管理會是下面的流程:

view controller -> RACDisposable -> RACSignal -> RACSubscriber -> view controller
           

然而, 

RACSignal -> RACSubscriber

 的關系會在信号完成時立刻解除,打破保留環。

這是你需要的,因為 

RACSignal

 的生命周期會自然而然的比對時間流的邏輯生命周期。

5.3 無限信号(Infinite Signals)

Infinite signals (or signals that live so long that they might as well be infinite), however, will never tear down naturally. This is where disposables shine.

Disposing of a subscription will remove the associated subscriber, and just

無限信号(或者說永遠存活的信号),不會被自動清除。

釋放訂閱會移除相關的訂閱者,并且會清除訂閱相關的任何資源。

作為一個一般的經驗法則,如果你需要手動管理訂閱的生命周期,那可能存在更好的方式做到你想要的,請避免顯示的訂閱和釋放。

5.4 從 

self

 分發的信号(Signals Derived from 

self

還是有些比較棘手的情況的。任何時候一個信号的生命周期被綁在一個調用範圍時,會比較難打破循環。

這種情況通常發生在關鍵路徑上使用 

RACObserve()

,而關鍵路徑又與 

self

 關聯,然後應用的 block 有需要捕獲 

self

。 

最簡單的解決方案是 捕獲 self 弱引用。

__weak id weakSelf = self;
[RACObserve(self, username) subscribeNext:^(NSString *username) {
    id strongSelf = weakSelf;
    [strongSelf validateUsername];
}];
           

或者,在導入 

EXTScope.h

 頭檔案之後:

@weakify(self);
[RACObserve(self, username) subscribeNext:^(NSString *username) {
    @strongify(self);
    [self validateUsername];
}];
           

如果對象不支援若引用,那麼用 

__unsafe_unretained

 或 

@unsafeify

 分别替換 

__weak

 或 

@weakify

 例如,上面的示例可以像下面這麼寫:

[self rac_liftSelector:@selector(validateUsername:) withSignals:RACObserve(self, username), nil];
           

或者:

RACSignal *validated = [RACObserve(self, username) map:^(NSString *username) {
    // Put validation logic here.
    return @YES;
}];
           

無限的信号,通常可以從信号鍊的 block 避開引用 

self

(或任何對象)。

上面的資訊是高效使用 ReactiveCocoa 所需要的一切。然而,還有很多隻為技術上的好奇心或者對 RAC 感興趣的聲音存在。

“不需要保留”的設計目标引入如下問題:我們如何知道信号什麼時候需要被釋放?如果信号剛建立,沒被自動釋放池管理,沒有被保留的話。

正确的回答是 

我們不需要

,但我們通常確定調用者如果想保留信号,會在目前運作循環疊代中保留它。

是以:

  1. 一個被建立的信号自動被添加到激活信号集合中。
  2. 信号會等一個主運作循環,然後如果沒有訂閱者訂閱它就會從激活信号集中移除。除非信号被什麼保留了,否則會被釋放。
  3. 如果在運作循環疊代中訂閱發生了,信号會留在集合中。
  4. 最後,如果所有訂閱者已過去,步驟2就會再被觸發。

如果運作循環是spun recursively(紡遞歸是什麼鬼?就像 OS X中的模态事件)那就适得其反了。但是讓架構的使用者使用友善比任何其他事情都重要。

繼續閱讀