1. 基本操作(Basic Operators)
描述 ReactiveCocoa 最常用的一些操作以及使用范例。 主要是如何运用 序列(sequences) 和 信号(signals) 的流操作。
用信号实现副作用(Performing side effects with signals)
- 订阅(Subscription)
- 依赖注入(Injecting effects)
流的传输(Transforming streams)
- 映射(Mapping)
- 过滤(Filtering)
流的结合(Combining streams)
- 串联(Concatenating)
- 压缩(Flattening)
- 映射和压缩(Mapping and flattening)
信号的结合(Combining signals)
- 排序(Sequencing)
- 合并(Merging)
- 结合最新值(Combining latest values)
- 切换(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)
__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
的约定
- 运算默认是懒执行模式。
- 运算会阻塞调用者。
- 副作用只发生一次。
RACSignal
的约定
最佳实践
- 为返回信号的方法或属性使用描述性的声明。
- 始终缩进流操作。
- 流的所有值都使用相同的类型。
- 不要retain流过长时间。
- 只处理需要数量的流。
- 分发信号事件到一个已知的调度。
- 较少的场合需要切换调度者。
- 明确信号的副作用。
- 通过multicasting共享信号的副作用。
- 通过指定的流的名字来调试。
- 避免明确的订阅(subscriptions)和释放(disposal)。
- 尽可能避免使用subjects。
完成新operators
- 优先使用基于 RACStream 方法。
- 尽可能组合已存在的operators。
- 避免引入并发。
- 在dispasable中取消任务和清理所有资源。
- 在operator中不要阻塞。
- 深度递归要避免栈溢出。
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",@"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
类型,会很难看懂信号的语义。
有三个关键问题必须在声明中表达清楚:
没有副作用的热信号应该典型的用属性来代替方法。使用属性意味着在订阅信号的时间之前不需要初始化,并且额外的订阅不会改变语义。 信号属性应该以事件命名(例如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 的操作(???这句话没整明白)。
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 常用于信号的取消订阅。 当订阅被释放,响应的订阅者不会再收到信号的任何未来事件。 并且任何订阅相关的工作(后台处理,网络请求等)都会取消,因为结果不再需要了。
关于取消的更多信息,参见 RACDesign 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 fromself
)
还是有些比较棘手的情况的。任何时候一个信号的生命周期被绑在一个调用范围时,会比较难打破循环。
这种情况通常发生在关键路径上使用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 感兴趣的声音存在。
“不需要保留”的设计目标引入如下问题:我们如何知道信号什么时候需要被释放?如果信号刚创建,没被自动释放池管理,没有被保留的话。
正确的回答是我们不需要
,但我们通常确保调用者如果想保留信号,会在当前运行循环迭代中保留它。
因此:
- 一个被创建的信号自动被添加到激活信号集合中。
- 信号会等一个主运行循环,然后如果没有订阅者订阅它就会从激活信号集中移除。除非信号被什么保留了,否则会被释放。
- 如果在运行循环迭代中订阅发生了,信号会留在集合中。
- 最后,如果所有订阅者已过去,步骤2就会再被触发。
如果运行循环是spun recursively(纺递归是什么鬼?就像 OS X中的模态事件)那就适得其反了。但是让框架的用户使用方便比任何其他事情都重要。