ReactiveCocoa简单介绍
26 Feb 2016
ReactiveCocoa简介
ReactiveCocoa is inspired by functional reactive programming. Rather than using mutable variables which are replaced and modified in-place, RAC offers “event streams,” represented by the Signal and SignalProducer types, that send values over time.
ReactiveCocoa的灵感来自函数式响应式编程(FRP)。RAC并不采用随时可变的变量,而是用事件流(表现为Signal
和SignalProducer
)的方式来捕捉值的变化。
Event streams unify all of Cocoa’s common patterns for asynchrony and event handling, including:
- Delegate methods
- Callback blocks
- NSNotifications
- Control actions and responder chain events
- Futures and promises
- Key-value observing (KVO)
在我们iOS开发过程中,经常会响应某些事件来处理某些业务逻辑,例如按钮的点击,上下拉刷新,网络请求,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。
其实这些事件,都可以通过RAC处理,ReactiveCocoa为事件提供了很多处理方法,而且利用RAC处理事件很方便,可以把要处理的事情,和监听的事情的代码放在一起,这样非常方便我们管理,就不需要跳到对应的方法里。非常符合我们开发中高聚合,低耦合的思想。
ReactiveCocoa常见类
RACSignal
信号类,一般表示将来有数据传递,只要有数据改变,信号内部接收到数据,就会马上发出数据。注意是,数据发出,并不是信号类发出。
-
信号类(RACSignal),只是表示当数据改变时,信号内部会发出数据,它本身不具备发送信号的能力,而是交给内部一个订阅者去发出。
-
默认一个信号都是冷信号,也就是值改变了,也不会触发,只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。
-
如何订阅信号:调用信号RACSignal的subscribeNext就能订阅。
RACSignal使用步骤:
- 创建信号
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
-
发送信号
- (void)sendNext:(id)value
- 订阅信号,才会激活信号
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
RACSignal底层实现:
-
创建子类信号
RACDynamicSignal
,首先把didSubscribe
这个block保存到信号RACDynamicSignal
中,但是还不会触发(冷信号)。 -
当信号被订阅,也就是调用
signal
的subscribeNext:nextBlock
。subscribeNext
内部会创建订阅者subscriber
,并且把nextBlock
保存到订阅者subscriber
中,此时为热信号。 -
subscribeNext
内部会调用RACDynamicSignal
的didSubscribe
这个block。通常也就是在RACDynamicSignal
的didSubscribe
中调用[subscriber sendNext:@1]
。 -
sendNext
底层其实就是执行订阅者subscriber
的nextBlock
。 -
标记信号发送完成或者取消订阅。
-
执行
RACDisposable
的disposeBlock
中的代码。
代码:
// 1.创建信号 createSignal:didSubscribe(block)
// RACDisposable:取消订阅
// RACSubscriber:发送数据
RACSignal *siganl = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// block调用时刻:每当有订阅者订阅信号,就会调用block。
// block作用:描述当前信号哪些数据需要发送
// _subscriber = subscriber;
// 3.发送信号
NSLog(@"调用了didSubscribe");
// 通常:传递数据出去
[subscriber sendNext:@1];
// 此处调用订阅者的nextBlock
// 5.如果不再发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。或者信号想要被取消,就必须返回一个RACDisposable,然后在后面[disposable dispose]
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
// 6.信号什么时候被取消:1.自动取消,当一个信号的订阅者被销毁的时候,就会自动取消订阅 2.主动取消
// block调用时刻:一旦一个信号,被取消订阅的时候就会调用
// block作用:当信号取消订阅,用于清空一些资源
NSLog(@"取消订阅");
}];
}];
// subscribeNext:创建订阅者,然后把nextBlock保存到订阅者里面
// 2.订阅信号:只要订阅信号,就会返回一个取消订阅信号的类
[siganl subscribeNext:^(id x) {
// 4.block调用时刻:每当有信号发出数据,就会调用block。
NSLog(@"接收到数据:%@",x);
}];
// 5.取消订阅
// [disposable dispose];
2016-03-19 21:51:02.587 RACDemo[44522:5148595] 调用了didSubscribe
2016-03-19 21:51:02.588 RACDemo[44522:5148595] 接收到数据:1
2016-03-19 21:51:02.589 RACDemo[44522:5148595] 取消订阅
RACSubscriber
表示订阅者的意思,用于发送信号,这是一个协议,不是一个类,只要遵守这个协议,并且实现方法才能成为订阅者。通过create创建的信号,都有一个订阅者,帮助他发送数据。
RACDisposable
用于取消订阅或者清理资源,当信号发送完成或者发送错误的时候,就会自动触发它。
RACSubject
信号提供者,自己可以充当信号,又能发送信号。
使用场景:通常用来代替代理,有了它,就不必要定义代理了。
RACSubject使用步骤
-
创建信号
[RACSubject subject]
,跟RACSignal
不一样,创建信号时没有block。 -
订阅信号
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
。 -
发送信号
sendNext:(id)value
。
RACSubject:底层实现和RACSignal不一样。
-
调用
subscribeNext
订阅信号,只是把订阅者保存起来,并且订阅者的nextBlock已经赋值了。 -
调用
sendNext
发送信号,遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock
。
代码:
// RACSubject:信号提供者
// 1.创建信号
RACSubject *subject = [RACSubject subject];
// 2.订阅信号
[subject subscribeNext:^(id x) {
// 4.block:当有数据发出的时候就会调用
// block:处理数据
NSLog(@"第一个订阅者%@",x);
}];
// 发送信号
// [subject sendNext:@1];
// 2.1 第二次订阅信号
[subject subscribeNext:^(id x) {
// block调用时刻:当信号发出新值,就会调用.
NSLog(@"第二个订阅者%@",x);
}];
// 3.发送信号
[subject sendNext:@1];
[subject sendNext:@2];
2016-03-19 22:00:09.573 RACDemo[44558:5150846] 第一个订阅者1
2016-03-19 22:00:09.574 RACDemo[44558:5150846] 第二个订阅者1
2016-03-19 22:00:09.575 RACDemo[44558:5150846] 第一个订阅者2
2016-03-19 22:00:09.575 RACDemo[44558:5150846] 第二个订阅者2
RACSubject
替换代理:
//在SubjectSecondViewController.h中添加一个RACSubject代替代理。
@property (nonatomic, strong) RACSubject *subject;
//监听SubjectSecondViewController的按钮点击
- (IBAction)backDidClick:(UIButton *)sender {
// 4.点击返回按钮时,便可以通知SubjectViewController做事情
if (self.subject) {
[self.subject sendNext:@1];
}
}
// 在SubjectViewController中,监听跳转按钮,给SubjectSecondViewController的代理信号赋值,并且监听
- (IBAction)buttonDidClick:(UIButton *)button {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
SubjectSecondViewController *twoVc = [storyboard instantiateViewControllerWithIdentifier:@"two"];
//1.创建并设置信号提供者
twoVc.subject = [RACSubject subject];
// 2.订阅信号
@weakify(twoVc)
[twoVc.subject subscribeNext:^(id x) {
NSLog(@"通知了ViewController:%@", x);
@strongify(twoVc)
[twoVc dismissViewControllerAnimated:YES completion:^{
}];
}];
// 3.present控制器
[self presentViewController:twoVc animated:YES completion:nil];
}
RACReplaySubject
重复提供信号类,RACSubject的子类。
使用场景一:如果一个信号每被订阅一次,就需要把之前的值重复发送一遍,使用重复提供信号类。
使用场景二:可以设置capacity
数量来限制缓存的value的数量,即只缓充最新的几个值。
- 创建信号
[RACReplaySubject subject]
,跟RACSignal
不一样,创建信号时没有block。 - 可以先订阅信号,也可以先发送信号。
- 订阅信号
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
- 发送信号
sendNext:(id)value
- 订阅信号
RACReplaySubject
:底层实现和``RACSubject不一样。
- 调用
sendNext
发送信号,把值保存起来,然后遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。 - 调用
subscribeNext
订阅信号,遍历保存的所有值,一个一个调用订阅者的nextBlock
。
如果想当一个信号被订阅,就重复播放之前所有值,需要先发送信号,再订阅信号。也就是先保存值,再订阅值。
代码:
// 1.创建信号
RACReplaySubject *subject = [RACReplaySubject subject];
// 2.订阅信号
[subject subscribeNext:^(id x) {
NSLog(@"第一个订阅者%@",x);
}];
// 3.发送信号
[subject sendNext:@1];
[subject sendNext:@2];
[subject subscribeNext:^(id x) {
NSLog(@"第二个订阅者%@",x);
}];
2016-03-19 22:02:33.387 RACDemo[44558:5150846] 第一个订阅者1
2016-03-19 22:02:33.387 RACDemo[44558:5150846] 第一个订阅者2
2016-03-19 22:02:33.389 RACDemo[44558:5150846] 第二个订阅者1
2016-03-19 22:02:33.390 RACDemo[44558:5150846] 第二个订阅者2
从输出中看出,无论sendNext
在订阅之前还是之后,输出不变。而RACSubject
同样的代码顺序则会输出:
2016-03-19 22:04:41.971 RACDemo[44572:5151777] 第一个订阅者1
2016-03-19 22:04:41.972 RACDemo[44572:5151777] 第一个订阅者2
也就是在sendNext
后面订阅的信号已经不管用了。
RACTuple
元组类,类似NSArra有,用来包装值。
RACTupleUnpack
宏:专门用来解析元组。等号右边:需要解析的元组。宏的参数:填解析数据的类型。元组里面有几个值,宏的参数就必须填几个。
// 遍历字典,遍历出来的键值对会包装成RACTuple(元组对象)
NSDictionary *dict = @{@"name":@"Tony", @"age":@18};
[dict.rac_sequence.signal subscribeNext:^(RACTuple *x) {
// 解包元组,会把元组的值,按顺序给参数里面的变量赋值
RACTupleUnpack(NSString *key, NSString *value) = x;
// 相当于以下写法
// NSString *key = x[0];
// NSString *value = x[1];
NSLog(@"%@ %@", key, value);
}];
2016-03-19 22:38:18.100 RACDemo[44687:5159036] name Tony
2016-03-19 22:38:18.105 RACDemo[44687:5159036] age 18
RACSequence
RAC中的集合类,用于代替NSArray和NSDictionary,可以使用它来快速遍历数组和字典。可以用来字典转模型等。
// 遍历数组
NSArray *arr = @[@1,@2,@3];
// 这里其实是三步
// 第一步: 把数组转换成集合RACSequence arr.rac_sequence
// 第二步: 把集合RACSequence转换RACSignal信号类,arr.rac_sequence.signal
// 第三步: 订阅信号,激活信号,会自动把集合中的所有值,遍历出来。
[arr.rac_sequence.signal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-19 22:34:23.459 RACDemo[44674:5158285] 1
2016-03-19 22:34:23.460 RACDemo[44674:5158285] 2
2016-03-19 22:34:23.461 RACDemo[44674:5158285] 3
字典转模型:
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];
NSArray *dictArr = [NSArray arrayWithContentsOfFile:filePath];
//OC
NSMutableArray *items = [NSMutableArray array];
for (NSDictionary *dict in dictArr) {
FlagItem *item = [FlagItem flagWithDict:dict];
[items addObject:item];
}
//RAC
NSMutableArray *flags = [NSMutableArray array];
// rac_sequence注意点:调用subscribeNext,并不会马上执行nextBlock,而是会等一会。
[dictArr.rac_sequence.signal subscribeNext:^(id x) {
// 运用RAC遍历字典,x:字典
FlagItem *item = [FlagItem flagWithDict:x];
[flags addObject:item];
}];
// 使用map
// map:映射的意思,目的:把原始值value映射成一个新值
// array: 把集合转换成数组
// 底层实现:当信号被订阅,会遍历集合中的原始值,映射成新值,并且保存到新的数组里。
NSArray *newFlags = [[dictArr.rac_sequence map:^id(id value) {
return [FlagItem flagWithDict:value];
}] array];
NSLog(@"%@, %@, %@", items, flags, newFlags);
RACCommand
RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。
使用场景:监听按钮点击,网络请求。
RACCommand
使用步骤:
- 创建命令
initWithSignalBlock:(RACSignal * (^)(id input))signalBlock
- 在
signalBlock
中,创建RACSignal
,并且作为signalBlock
的返回值 - 执行命令
- (RACSignal *)execute:(id)input
RACCommand
使用注意:
signalBlock
必须要返回一个信号,不能传nil。- 如果不想要传递信号,直接创建空的信号
[RACSignal empty]
。 RACCommand
中信号如果数据传递完,必须调用[subscriber sendCompleted]
,这时命令才会执行完毕,否则永远处于执行中。RACCommand
需要被强引用,否则接收不到RACCommand
中的信号,因此RACCommand
中的信号是延迟发送的。
RACCommand
设计思想:内部signalBlock
为什么要返回一个信号,这个信号有什么用。
- 在RAC开发中,通常会把网络请求封装到
RACCommand
,直接执行某个RACCommand
就能发送请求。 - 当
RACCommand
内部请求到数据的时候,需要把请求的数据传递给外界,这时候就需要通过``signalBlock返回的信号传递了。
- 如何拿到
RACCommand
中返回信号发出的数据。
RACCommand
有个执行信号源executionSignals
,这个是``signal of signals(信号的信号),意思是信号发出的数据是信号,不是普通的类型。- 订阅``executionSignals就能拿到
RACCommand
中返回的信号,然后订阅signalBlock
返回的信号,就能获取发出的值。
- 监听当前命令是否正在执行executing。
// 创建命令类
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
// block什么时候调用:当执行这个命令类的时候就会调用
NSLog(@"执行命令 %@", input);
// block有什么作用:描述下如何处理事件,网络请求
// 创建空信号,必须返回信号
// return [RACSignal empty];
// 2.RACCommand必须返回信号,处理事件的时候,肯定会有数据产生,产生的数据就通过返回的信号发出。注意:数据传递完,最好调用sendCompleted,这时命令才执行完毕。
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// block作用:发送处理事件的信号
// block调用:当信号被订阅的时候才会调用
[subscriber sendNext:@"信号发出的内容"];
[subscriber sendCompleted];
return nil;
}];
}];
// 强引用命令,不要被销毁,否则接收不到数据
_command = command;
// executionSignals:信号源,包含事件处理的所有信号。
// executionSignals: signalOfSignals,信号中的信号,就是信号发出的数据也是信号类
// 3.如果想要订阅接收信号源的信号内容,必须保证命令类不会被销毁
[command.executionSignals subscribeNext:^(id x) {
// x -> 信号
[x subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
}];
// 4.执行命令,调用signalBlock
[command execute:@1];
// RAC高级用法
// switchToLatest:用于signal of signals,获取signal of signals发出的最新信号,也就是可以直接拿到RACCommand中的信号
[command.executionSignals.switchToLatest subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
// 5.监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次信号。
[[command.executing skip:1] subscribeNext:^(id x) {
if ([x boolValue] == YES) {
// 正在执行
NSLog(@"正在执行");
}else{
// 执行完成
NSLog(@"执行完成");
}
}];
RACMulticastConnection
用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理。
使用注意:RACMulticastConnection
通过RACSignal
的- (RACMulticastConnection *)publish或
者- (RACMulticastConnection *)multicast:(RACSubject *)subject
方法创建.
RACMulticastConnection
使用步骤:
- 创建信号
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
- 创建连接
RACMulticastConnection *connect = [signal publish];
- 订阅信号。注意:订阅的不在是之前的信号,而是连接的信号。
[connect.signal subscribeNext:nextBlock]
- 连接
[connect connect]
RACMulticastConnection
底层原理:
- 创建
connect
,connect.sourceSignal
->RACSignal
(原始信号)connect.signal
->RACSubject
。 - 订阅
connect.signal
,会调用RACSubject的subscribeNext
,创建订阅者,而且把订阅者保存起来,不会执行block。 [connect connect]
内部会订阅RACSignal(原始信号),并且订阅者是RACSubject。- 订阅原始信号,就会调用原始信号中的
didSubscribe
。 didSubscribe
,拿到订阅者调用sendNext
,其实是调用RACSubject
的`sendNext
- 订阅原始信号,就会调用原始信号中的
RACSubject
的sendNext
,会遍历RACSubject
所有订阅者发送信号。- 因为刚刚第二步,都是在订阅
RACSubject
,因此会拿到第二步所有的订阅者,调用他们的nextBlock
- 因为刚刚第二步,都是在订阅
需求:假设在一个信号中发送请求,每次订阅一次都会发送请求,这样就会导致多次请求。 解决:使用RACMulticastConnection就能解决.
// 发送请求,用一个信号内包装,不管有多少个订阅者,只想要发送一次请求
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// didSubscribe(block)中的代码都统称为副作用(Side Effects)。
// 发送请求
NSLog(@"发送请求");
[subscriber sendNext:@1];
return nil;
}];
// 订阅信号
[signal subscribeNext:^(id x) {
NSLog(@"接收数据:%@",x);
}];
[signal subscribeNext:^(id x) {
NSLog(@"接收数据:%@",x);
}];
2016-03-20 13:02:59.130 RACDemo[45388:5236769] 发送请求
2016-03-20 13:02:59.134 RACDemo[45388:5236769] 接收数据:1
2016-03-20 13:02:59.134 RACDemo[45388:5236769] 发送请求
2016-03-20 13:02:59.135 RACDemo[45388:5236769] 接收数据:1
运行结果,会执行两遍发送请求,也就是每次订阅都会发送一次请求。
// RACMulticastConnection:解决重复请求问题
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"发送请求");
[subscriber sendNext:@1];
return nil;
}];
// 2.创建连接
RACMulticastConnection *connect = [signal publish];
// 3.订阅信号,
// 注意:订阅信号,也不能激活信号,只是保存订阅者到数组,必须通过连接,当调用连接,就会一次性调用所有订阅者的sendNext:
[connect.signal subscribeNext:^(id x) {
NSLog(@"订阅者一信号");
}];
[connect.signal subscribeNext:^(id x) {
NSLog(@"订阅者二信号");
}];
// 4.连接,激活信号
[connect connect];
2016-03-20 13:05:13.803 RACDemo[45398:5237395] 发送请求
2016-03-20 13:05:13.804 RACDemo[45398:5237395] 订阅者一信号
2016-03-20 13:05:13.804 RACDemo[45398:5237395] 订阅者二信号
RACSchedule
RAC中的队列,用GCD封装的。
RACUnit
表⽰stream不包含有意义的值,也就是看到这个,可以直接理解为nil。
RACEvent
把数据包装成信号事件(signal event
)。它主要通过RACSignal的-materialize
来使用。
参考:
代码:
文章中的代码都可以从我的GitHub RACDemo
找到。