ReactiveObjC中的冷热信号与命令

一如标题,本篇主要介绍ReactiveObjC中的冷热信号与命令。

按照惯例,先来一张图镇帖。

冷热信号

首先来复习一下标准的信号订阅流程:

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    NSLog(@"Signal begin sending.");
    [subscriber sendNext:@"🐱"];
    [subscriber sendNext:@"🐶"];
    [subscriber sendNext:@"🐰"];
    [subscriber sendCompleted];
    return nil;
}];
NSLog(@"Signal was created.");
[[RACScheduler mainThreadScheduler] afterDelay:0.2 schedule:^{
    // Subscription 1
    [signal subscribeNext:^(id x) {
        NSLog(@"Subscription 1 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
    // Subscription 2
    [signal subscribeNext:^(id x) {
        NSLog(@"Subscription 2 recveive: %@", x);
    }];
}];

结果是:

0.0      Signal was created.
0.2      Signal begin sending.
0.2      Subscription 1 recveive: 🐱
0.2      Subscription 1 recveive: 🐶
0.2      Subscription 1 recveive: 🐰
1.0      Signal begin sending.
1.0      Subscription 2 recveive: 🐱
1.0      Subscription 2 recveive: 🐶
1.0      Subscription 2 recveive: 🐰

解释一下:

  1. [RACScheduler mainThreadScheduler] afterDelay:1 schedule:... 是RAC中负责线程管理、延时执行的类和方法。下篇文章会详细介绍,暂时先忽略,如果觉得奇怪完全可以用GCD替换;
  2. 打印结果前面的数字代表的是此行代码执行距离最初代码执行所经历过的时间差;
  3. 延时执行代码的意义主要是为了使执行结果更加清晰,方便解释与理解;

观察结果可以发现:

  1. 只有当信号被订阅之后创建信号时block中的代码段才会被执行;
  2. 同一个信号先后被多次订阅后,信号创建时block中的代码段会执行多次,也就是会多次发送完整信息流;
  3. 每个订阅者彼此独立,互不影响,只要订阅了源信号,就会完整接收到其发送的整个信息流;

示意图如下:

那么问题来了,这种方式有什么弊端或者说在什么情况下不合适呢?

一切取决于信号发送事件的机制,上面例子中只是发送几个字符串,对外界并不会产生影响,所以没什么问题。但是考虑这么一种情况:信号被订阅后会触发一个网络请求,然后根据请求结果决定对外发送的数据,两个订阅者分别是两个UI控件,它们等待信号发出数据然后进行刷新。此时用上面这种方式就会使网络请求发生两次,从而造成流量浪费,统计错误,甚至UI刷新不一致等一系列问题。

怎么样解决呢?这就引出了本节的主角——“RACSubject”。

RACSubject

首先来看一下定义:

RACSubject : RACSignal <RACSubscriber>

首先,它是RACSignal的子类,所以归根结底也是一个信号;同时实现了RACSubscriber协议,所以本身具有发送事件的能力。

如何初始化呢?也很简单:

+ (instancetype)subject;

直接看代码:

RACSubject *subject = [RACSubject subject];
NSLog(@"Subject was created.");
[[RACScheduler mainThreadScheduler] afterDelay:0.2 schedule:^{
    // Subscription 1
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscription 1 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
    NSLog(@"Subject begin sending first value.");
    [subject sendNext:@"🐸"];
    [subject sendNext:@"🐟"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1.2 schedule:^{
    // Subscription 2
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscription 2 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
    NSLog(@"Subject begin sending second value.");
    [subject sendNext:@"🐍"];
}];

结果是:

0.0      Subject was created.
1.0      Subject begin sending first value.
1.0      Subscription 1 recveive: 🐸
1.0      Subscription 1 recveive: 🐟
2.0      Subject begin sending second value.
2.0      Subscription 1 recveive: 🐍
2.0      Subscription 2 recveive: 🐍 // 第一波没赶上,只能等第二波

解释一下:

  1. 调用类方法subject创建一个RACSubject类的实例对象subject
  2. subject是一个signal,所以可以被订阅;
  3. subject实现了RACSubscriber协议,所以可以调用sendNext:等方法发送事件;

观察结果可以发现:

  1. 两个订阅者分别在0.2s和1.2s订阅了源信号subjectsubject分别在1s和2s发送事件,其中1s时发送2个,2s时发送1个;
  2. 订阅者1在三个值发送之前就已经订阅了,所以3个值全部接收到,而订阅者2在前两个值发送之后才开始订阅,所以只接收到最后一个值;
  3. 源信号subject发送事件与否与有没有订阅者、有多少订阅者完全无关,并且发送事件次数与订阅者个数也无关;

示意图如下:


上面两段代码以及执行结果进行对比可以发现RACSignal和RACSubject的区别:

  1. 同为信号,RACSignal不具有发送事件的能力,而RACSubject有;
  2. RACSignal只有被订阅后才能发送事件,而RACSubject不受此限制;
  3. 订阅RACSignal的不同订阅者之间是彼此独立的,都能接收到源RACSignal发送的完整信息流;而订阅RACSubject的不同订阅者之间是彼此共享的,共同接收一个信息流。

什么是冷信号与热信号

下面说一个在RAC中被广泛讨论的问题,什么是冷信号与热信号?

冷热信号的概念源于.NET框架Reactive Extensions.aspx)中的Hot Observable和Cold Observable:

Cold vs. Hot Observables

Cold observables start running upon subscription, i.e., the observable sequence only starts pushing values to the observers when Subscribe is called. Values are also not shared among subscribers. This is different from hot observables such as mouse move events or stock tickers which are already producing values even before a subscription is active. When an observer subscribes to a hot observable sequence, it will get the current value in the stream. The hot observable sequence is shared among all subscribers, and each subscriber is pushed the next value in the sequence.

读了这段说明再去看一下上面提到的RACSignal和RACSubject两者间的区别就会发现:RACSignal对应上面的“Cold observable”(也就是冷信号),而RACSubject对应上面的“Hot observable”(也就是热信号)。如果把能否发送事件作为一个信号是否被激活的判断标准,那么一个冷信号刚被创建时还是冷却状态,只有被订阅后才会被激活;而一个热信号创建之初就是激活状态。两者最核心的区别在于——是否“共享”(信息流)。

举个不是十分恰当的例子:它们之间有点像火车和出租车的关系。一列火车出发的时间点以及经过哪些站都是提前就定好了的(忽略晚点及其他特殊情况),它不会因有没有乘客有多少乘客而改变,所有乘客共享这一列火车,一个乘客若上一站没有赶上则只能去下一站上车了;而出租车呢,任何一定时间内对于一个乘客而言都是唯一的(暂不考虑多人共打一辆车的情况),A想从海淀去朝阳需要打一辆车,B也想从海淀去朝阳则需要打另外一辆车,它们彼此之间相互独立,不共享。

很自然会想到一个新问题,为什么要区分冷热信号?

这要从函数式编程(FR)说起,上一篇文章中写过这么一句话:

其中每一个函数都是稳定无副作用的,表现在:在任意时刻输入相同的值,内部经过运算后都会输出相同的值,不会对外界产生任何影响。

这句话现在可以详细讲一讲了。

FP中有个非常重要的概念——纯函数。所谓纯函数就是返回值只由输入值决定、而且没有可见副作用的函数。

什么是副作用呢?就是函数内部对外界产生的影响。举几个iOS中常见的🌰:

修改全局变量,修改静态变量,修改对象的成员变量,发送通知,将数据写入文件,触发网络请求等等。

你会发现之前的编程中充满了各种各样的副作用,而这就就是OOP的特点。

产生副作用是好的,但产生多次副作用可能就是坏的了。

此外,纯函数当中还有一个概念——引用透明,是指在相同参数条件下第二次及以后获取返回值纯函数不需要进行重复计算。这主要是为了性能考虑。

综合这两个因素,为了解决上面提到的“共享”难题,冷热信号应运而生。

其实,只要清楚RACSignal和RACSubject各自的特点和区别,冷热信号的概念即使不完全清楚依然不影响使用。

RACSubject两个常用子类

另外,RACSubject有两个常用的子类,分别是 RACReplaySubjectRACBehaviorSubject,简单介绍下。

RACReplaySubject

来看一下初始化方法:

+ (instancetype)replaySubjectWithCapacity:(NSUInteger)capacity;

直接看代码:

RACReplaySubject *replaySubject = [RACReplaySubject replaySubjectWithCapacity:RACReplaySubjectUnlimitedCapacity];
NSLog(@"ReplaySubject was created.");
[[RACScheduler mainThreadScheduler] afterDelay:0.2 schedule:^{
    // Subscription 1
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscription 1 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
    NSLog(@"ReplaySubject begin sending first value.");
    [replaySubject sendNext:@"🐸"];
    [replaySubject sendNext:@"🐟"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1.2 schedule:^{
    // Subscription 2
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscription 2 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
    NSLog(@"ReplaySubject begin sending second value.");
    [replaySubject sendNext:@"🐍"];
}];

结果是:

0.0      ReplaySubject was created.
1.0      ReplaySubject begin sending first value.
1.0      Subscription 1 recveive: 🐸
1.0      Subscription 1 recveive: 🐟
1.2      Subscription 2 recveive: 🐸  // 第一波虽然没赶上,但是也能收到
1.2      Subscription 2 recveive: 🐟  // 。。。
2.0      ReplaySubject begin sending second value.
2.0      Subscription 1 recveive: 🐍
2.0      Subscription 2 recveive: 🐍

解释一下:

  1. 调用类方法replaySubjectWithCapacity:创建一个RACReplaySubject类的实例对象replaySubject,参数指的是当有新的订阅者订阅时重复发送的历史事件的个数,RACReplaySubjectUnlimitedCapacity代表无穷大;

观察结果可以发现:

  1. 两个订阅者分别在0.2s和1.2s订阅了源信号replaySubjectreplaySubject分别在1s和2s发送事件,其中1s时发送2个,2s时发送1个,和上面一样;
  2. 订阅者1在三个值发送之前就已经订阅了,所以3个值全部接收到,很好理解;
  3. 订阅者2在前两个值发送之后才开始订阅,但是订阅后却立刻收到了这两个值,这也就是“replay”的含义;
  4. 最后一个值在订阅者2订阅后才发送,所以订阅者2可以正常实时接收到;

示意图如下:

RACBehaviorSubject

来看一下初始化方法:

+ (instancetype)behaviorSubjectWithDefaultValue:(id)value;

直接看代码:

RACBehaviorSubject *behaviorSubject = [RACBehaviorSubject behaviorSubjectWithDefaultValue:@"🐢"];
NSLog(@"BehaviorSubject was created.");
[[RACScheduler mainThreadScheduler] afterDelay:0.2 schedule:^{
    // Subscription 1
    [behaviorSubject subscribeNext:^(id x) {
        NSLog(@"Subscription 1 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
    NSLog(@"ReplaySubject begin sending first value.");
    [behaviorSubject sendNext:@"🐸"];
    [behaviorSubject sendNext:@"🐟"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1.2 schedule:^{
    // Subscription 2
    [behaviorSubject subscribeNext:^(id x) {
        NSLog(@"Subscription 2 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
    NSLog(@"ReplaySubject begin sending second value.");
    [behaviorSubject sendNext:@"🐍"];
}];

结果是:

0.0      BehaviorSubject was created.
0.2      Subscription 1 recveive: 🐢  // 收到初始化值
1.0      ReplaySubject begin sending first value.
1.0      Subscription 1 recveive: 🐸
1.0      Subscription 1 recveive: 🐟
1.2      Subscription 2 recveive: 🐟  // 第一波没赶上,但是能收到最后一个
2.0      ReplaySubject begin sending second value.
2.0      Subscription 1 recveive: 🐍
2.0      Subscription 2 recveive: 🐍

解释一下:

  1. 调用类方法behaviorSubjectWithDefaultValue:创建一个RACBehaviorSubject类的实例对象behaviorSubject,顾名思义,参数指的是默认值,当其被订阅时首先会发送这个默认值(若没有发送过其它数据);

观察结果可以发现:

  1. 两个订阅者分别在0.2s和1.2s订阅了源信号behaviorSubjectbehaviorSubject分别在1s和2s发送事件,其中1s时发送2个,2s时发送1个,仍然和上面一样;
  2. 订阅者1在信号还未发送过任何事件时就订阅了,却立刻收到了信号初始化时传入的默认值;
  3. 之后正常实时接收到信号发送的3个值,就和上面一样了,很好理解;
  4. 订阅者2在前两个值发送之后才开始订阅,但是订阅后却立刻收到了之前发送过的最后一个值;
  5. 最后一个值在订阅者2订阅后才发送,所以订阅者2可以正常实时接收到;

示意图如下:

两者对比:

RACBehaviorSubjectRACReplaySubject其实非常像,区别就在于:RACReplaySubject可以设置重复发送事件的数量,而RACBehaviorSubject只能重复发送一个,但RACBehaviorSubject可以设置一个初始值。

顺便提一下,RAC官方在文档中说明不推荐使用RACSubject,因为它过于灵活,滥用的话很容易导致复杂度的增加。

RACMulticastConnection

有的博客中会提出这么一个问题:如何将一个冷信号转化成热信号?在我看来,这其实就是个伪命题。正确的问题应该是:如何让多个订阅者共享的订阅一个冷信号?

这就引出了一个新类——“RACMulticastConnection”。

其实非常简单,创建一个RACSubject的实例对象subject,用此subject去订阅源冷信号RACSignal的实例对象signal,signal发出的每一个事件通过subject再发送出去,而外界只需要订阅这个subject。需要注意的就是要保证subject对signal的订阅次数不超过一次。

原理如图所示:

所以我才说“将一个冷信号转化成热信号”是伪命题,因为从始至终源冷信号RACSignal都是冷信号,没变过,何谈转换?移花接木不等于改头换面。

来看看怎么使用,首先看定义:

一个属性

@property (nonatomic, strong, readonly) RACSignal *signal;

两个方法

- (RACDisposable *)connect;
- (RACSignal *)autoconnect;

注意并没有初始化方法!!!

那如何创建一个RACMulticastConnection的实例对象呢?为此,RACSignal中定义了如下两个方法:

- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACMulticastConnection *)publish;

下面详细介绍一下。

首先,定义一个辅助方法以方便使用。

- (RACSignal *)sourceColdSignal {
    RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"Cold signal be subscribed.");
        [[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
            [subscriber sendNext:@"🦅"];
        }];
        [[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
            [subscriber sendNext:@"🐊"];
        }];
        [[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{
            [subscriber sendNext:@"🐺"];
            [subscriber sendCompleted];
        }];
        return nil;
    }];
    return coldSignal;
}

就是简单创建一个signal,当被订阅后,间隔1s,2s,3s分别发送一个值。

multicast

直接看代码:

RACMulticastConnection *connection = [[self sourceColdSignal] multicast:[RACSubject subject]];

解释一下:

  1. 调用上面的辅助方法得到一个冷信号coldSignal
  2. 对冷信号coldSignal调用方法multicast:可创建一个RACMulticastConnection类的实例对象,其中参数是一个RACSubject类型的对象;
  3. 上面方法中使用的是RACSubject类的对象,当然也可以使用RACBehaviorSubjectRACReplaySubject的对象;

publish

直接看代码:

RACMulticastConnection *connection = [[self sourceColdSignal] publish];

解释一下:

  1. 同样调用上面的辅助方法得到一个冷信号coldSignal
  2. 对冷信号coldSignal调用方法publish可创建一个RACMulticastConnection类的实例对象,不需要传参数;
  3. publish方法内部会创建一个RACSubject类的对象,然后调用multicast:方法,所以上面这两个方法其实是等价的;

下面介绍一个属性和两个方法如何使用。

connect

直接看代码:

RACMulticastConnection *connection = [[self sourceColdSignal] publish];
NSLog(@"MulticastConnection was created.");
RACSignal *hotSignal = connection.signal;
NSLog(@"Hot Signal was got.");
[[RACScheduler mainThreadScheduler] afterDelay:0.2 schedule:^{
    [connection connect];
}];
[[RACScheduler mainThreadScheduler] afterDelay:1.0 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 1 recieve: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2.0 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 2 recieve: %@", x);
    }];
}];

结果是:

0.0      MulticastConnection was created.
0.0      Hot Signal was got.
0.2      Cold signal be subscribed.
1.2      Subscription 1 recieve: 🦅
2.2      Subscription 1 recieve: 🐊
2.2      Subscription 2 recieve: 🐊
3.2      Subscription 1 recieve: 🐺
3.2      Subscription 2 recieve: 🐺   

解释一下:

  1. 调用connectionsignal属性得到一个热信号hotSignal,供订阅者去订阅;
  2. 调用connectionconnect方法去激活源冷信号;

观察结果可以发现:

  1. connection对象在0.2s时调用了connect方法,两个订阅者分别在1.0s和2.0s时订阅了热信号hotSignal
  2. 源冷信号coldSignal创建时block中的代码段不会执行直到connection调用了connect方法,并且和订阅者数量无关,从始至终只执行了一遍;
  3. 订阅者1在三个值发送之前就已经订阅了,所以3个值全部实时接收到,而订阅者2在第一个值发送之后才订阅,所以从第二个值开始接收;

示意图如下:

autoconnect

直接看代码:

RACMulticastConnection *connection = [[self sourceColdSignal] publish];
NSLog(@"MulticastConnection was created.");
RACSignal *hotSignal = [connection autoconnect];
NSLog(@"Hot Signal was got.");
[[RACScheduler mainThreadScheduler] afterDelay:1.0 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 1 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2.5 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 2 recveive: %@", x);
    }];
}];

结果是:

0.0      MulticastConnection was created.
0.0      Hot Signal was got.
1.0      Cold signal be subscribed.
2.0      Subscription 1 recveive: 🦅
3.0      Subscription 1 recveive: 🐊
3.0      Subscription 2 recveive: 🐊
4.0      Subscription 1 recveive: 🐺
4.0      Subscription 2 recveive: 🐺

解释一下:

  1. 调用connectionautoconnect方法直接得到一个热信号hotSignal,供订阅者去订阅;

观察结果可以发现:

  1. connection对象创建之后立刻调用autoconnect方法得到一个热信号hotSignal,两个订阅者分别在1.0s和2.5s时订阅了它;
  2. 源冷信号coldSignal创建时block中的代码段不会执行直到第一次被订阅,并且和订阅者数量无关,从始至终只执行了一遍;
  3. 订阅者1在三个值发送之前就已经订阅了,所以3个值全部实时接收到,而订阅者2在第一个值发送之后才订阅,所以从第二个值开始接收,和上面一样;

示意图如下:

三个简化使用操作符

如果你觉得上面做法很麻烦,那么恭喜你,说明你已经懂了!有没有更简单的使用方式呢?那绝对是当然的。RACSignal中还定义了如下三个操作符:

- (RACSignal *)replay;
- (RACSignal *)replayLast;
- (RACSignal *)replayLazily;

首先,还是定义一个辅助方法。

- (RACSignal *)sourceColdSignal2 {
    RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"Cold signal be subscribed.");
        [[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
            [subscriber sendNext:@"🐊"];
            [subscriber sendNext:@"🐬"];
        }];
        [[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
            [subscriber sendNext:@"🦈"];
            [subscriber sendCompleted];
        }];
        return nil;
    }];
    return coldSignal;
}

还是简单创建一个signal,当被订阅后,间隔1s发送两个值,2s发送一个值。

replay

直接看代码:

RACSignal *coldSignal = [self sourceColdSignal2];
NSLog(@"Cold Signal was created.");
RACSignal *hotSignal = [coldSignal replay]; // 立刻connect
NSLog(@"Hot Signal was got.");
[[RACScheduler mainThreadScheduler] afterDelay:1.2 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 1 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2.2 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 2 recveive: %@", x);
    }];
}];

结果是:

0.0      Cold Signal was created.
0.0      Cold signal be subscribed.
0.0      Hot Signal was got.
1.2      Subscription 1 recveive: 🐊
1.2      Subscription 1 recveive: 🐬
2.0      Subscription 1 recveive: 🦈 // ✨只有这个是实时收到的,其余几个全是收到的历史值
2.2      Subscription 2 recveive: 🐊
2.2      Subscription 2 recveive: 🐬
2.2      Subscription 2 recveive: 🦈   

replayLast

直接看代码:

RACSignal *coldSignal = [self sourceColdSignal2];
NSLog(@"Cold Signal was created.");
RACSignal *hotSignal = [coldSignal replayLast]; // 立刻connect
NSLog(@"Hot Signal was got.");
[[RACScheduler mainThreadScheduler] afterDelay:1.2 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 1 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2.2 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 2 recveive: %@", x);
    }];
}];

结果是:

0.0      Cold Signal was created.
0.0      Cold signal be subscribed.
0.0      Hot Signal was got.
1.2      Subscription 1 recveive: 🐬
2.0      Subscription 1 recveive: 🦈 // ✨这个是实时收到,剩下两个是历史值;注意数量区别
2.2      Subscription 2 recveive: 🦈

replayLazily

直接看代码:

RACSignal *coldSignal = [self sourceColdSignal2];
NSLog(@"Cold Signal was created.");
RACSignal *hotSignal = [coldSignal replayLazily]; // 延迟connect
NSLog(@"Hot Signal was got.");
[[RACScheduler mainThreadScheduler] afterDelay:1.2 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 1 recveive: %@", x);
    }];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2.4 schedule:^{
    [hotSignal subscribeNext:^(id x) {
        NSLog(@"Subscription 2 recveive: %@", x);
    }];
}];

结果是:

0.0      Cold Signal was created.
0.0      Hot Signal was got.
1.2      Cold signal be subscribed.  // ✨ 首次订阅之后才会connect    
2.2      Subscription 1 recveive: 🐊
2.2      Subscription 1 recveive: 🐬
2.4      Subscription 2 recveive: 🐊 // 历史值
2.4      Subscription 2 recveive: 🐬 // 历史值
3.2      Subscription 1 recveive: 🦈
3.2      Subscription 2 recveive: 🦈

所谓万变不离其宗,相信聪明的你已经了然于胸,就不过多解释了。

命令

命令——RACCommand,通常用来表示某个Action的执行,比如点击一个Button。合理使用的话可以很大程度上提高代码的可读性与健壮性。

头文件中有如下内容:

属性:

@property (nonatomic, strong, readonly) RACSignal *executionSignals;
@property (nonatomic, strong, readonly) RACSignal *executing;
@property (nonatomic, strong, readonly) RACSignal *errors;

初始化方法:

- (id)initWithSignalBlock:(RACSignal * (^)(id input))signalBlock;
- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock;

执行方法:

- (RACSignal *)execute:(id)input;

直接解释略显生硬,还是先来看一个完整的例子:

@property (weak, nonatomic) IBOutlet UITextField *usernameTextField;
@property (weak, nonatomic) IBOutlet UITextField *passwordTextField;
@property (weak, nonatomic) IBOutlet UIButton *loginButton;
@property (weak, nonatomic) IBOutlet UILabel *hintInfoLabel;

@property (nonatomic, strong) RACCommand *loginCommand;


- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"RACCommand Use Demo";

    RACSignal *usernameValidSignal = [self.usernameTextField.rac_textSignal map:^id(NSString *text) {
        return @([text isValidUsername]);
    }];
    RACSignal *passwordValidSignal = [self.passwordTextField.rac_textSignal map:^id(NSString *text) {
        return @([text isValidPassword]);
    }];
    RACSignal *loginEnableSignal = [RACSignal combineLatest:@[usernameValidSignal, passwordValidSignal]
                                                     reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid){
                                                         return @([usernameValid boolValue] && [passwordValid boolValue]);
                                   }];

    RAC(self.usernameTextField, backgroundColor) = [usernameValidSignal map:^id(NSNumber *valid) {
        return [valid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
    }];
    RAC(self.passwordTextField, backgroundColor) = [passwordValidSignal map:^id(NSNumber *valid) {
        return [valid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
    }];
    RAC(self.loginButton, enabled) = loginEnableSignal;

    @weakify(self);
    // 执行登录的信号
    RACSignal *loginSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        @strongify(self);
        [LoginService loginWithUsername:self.usernameTextField.text password:self.passwordTextField.text result:^(NSString *nickname, NSError *error) {
            if (!error) {
                [subscriber sendNext:nickname];
                [subscriber sendCompleted];
            }
            else {
                [subscriber sendError:error];
            }
        }];
        return nil;
    }];
    // 初始化的时候创建一个用于执行的信号,同时还可以传入一个用于控制是否允许执行的信号(非必需)
    RACCommand *loginCommand = [[RACCommand alloc] initWithEnabled:loginEnableSignal signalBlock:^RACSignal *(id input) {
        NSLog(@"get input value is %@",input);
        // 创建信号的时候可以使用调用执行方法时传来的参数:input
        return loginSignal;
    }];
    // 这里仅处理命令是否正在执行的回调
    [[loginCommand.executing skip:1] subscribeNext:^(id x) {
        if ([x boolValue] == YES) {
            [SVProgressHUD show];
        } else {
            [SVProgressHUD dismiss];
        }
    }];
    // 这里仅处理命令执行成功的回调
    [loginCommand.executionSignals.switchToLatest subscribeNext:^(NSString *nickname) {
        @strongify(self);
        self.hintInfoLabel.text = [NSString stringWithFormat:@"%@,欢迎回来!",nickname];
        self.loginButton.enabled = YES;
    }];
    // 这里仅处理命令执行失败的回调
    [loginCommand.errors subscribeNext:^(NSError *error) {
        @strongify(self);
        self.hintInfoLabel.text = [NSString stringWithFormat:@"%@",[error.userInfo objectForKey:@"hint"]];
        self.loginButton.enabled = YES;
    }];
    self.loginCommand = loginCommand;

    [[[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside]
      doNext:^(id x) {
          @strongify(self);
          [self.view endEditing:YES];
          self.loginButton.enabled = NO;
          self.hintInfoLabel.text = @"";
      }]
      subscribeNext:^(id x) {
          @strongify(self);
          // 触发执行的时候可以传入一个对象
          [self.loginCommand execute:@"🍎"];
     }];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.view endEditing:YES];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.view endEditing:YES];
}

解释一下:

  1. 模拟一个最简单的登录页面,两个输入框分别用来输入用户名和密码,均验证通过后下面登录按钮才可点击,点击会触发登录请求,根据请求结果不同按钮下面会显示不同的提示文案;
  2. 5个属性:其中4个UI控件,1个RACCommand的实例对象;三个方法:后两个都是为了控制键盘适时收起,可以忽略。所以只需要重点看viewDidLoad一个方法;
  3. 首先创建3个用于后续判断的信号,然后分别设置3个UI控件的属性值。其中rac_textSignal信号是RAC为UITextField类所做的拓展属性,RAC(TARGET, KEYPATH)是RAC为了简化赋值操作所创造的宏定义;
  4. @weakify(self)@strongify(self)搭配使用是为了避免循环引用;
  5. 创建一个用于登录的信号实例loginSignal,内部“包裹”着一个网络请求,成功则送出登录用户的昵称,否则送出错误原因;
  6. 创建一个用于执行登录命令的RACCommand实例loginCommand,这里选择的是复杂的初始化方法,第一个参数用于控制这个命令是否满足条件去执行,第二个参数是一个返回信号的block,这个信号就是命令的内容(可以理解成一个命令内部“包裹”着一个信号),需要注意的是block有一个入参input可以供创建信号时使用;
  7. executing属性仅对外发送此命令是否正在执行,因为默认值是NO,所以实际订阅时可以skip一下;
  8. executionSignals属性仅在命令执行成功时对外发送事件,需要注意的是事件本身也是一个信号,也就是信号中的信号(signalOfSignal),所以使用时需要调用switchToLatest操作符一下,取得实际想要的值;
  9. errors属性仅在命令执行失败时对外发送事件,需要注意的是虽然是NSError类型的数据发送的却是Next类型事件;
  10. 调用loginCommandexecute:方法去执行这个命令,需要注意的是此时可以传入一个对象;
  11. execute:方法的返回值也是一个信号,所以当然也可以直接订阅它,不过一般不这么干,有兴趣的可以去订阅一下,将其发送的值和三个属性信号发送的值对比一下;

来看一下执行过程,初始状态:

点击登录按钮后:

登录成功后:

登录失败后:

总结:将一个表示行为的信号signal放到一个命令command中,提前设置好分别订阅此command的三个属性信号以应对执行过程与结果,调用execute方法去执行此command。通过这种方式代码变得相当清晰有木有?!

另外,RAC为UIButton类也拓展了一个信号属性rac_command,对于某些情况还可以直接将command实例赋值给UIButton对象的rac_command属性,这样enable状态,以及点击时触发执行就都不用管了,相当方便,下篇文章会详细介绍。

顺便提一下,RACCommand类其实还有两个属性如下:

@property (nonatomic, strong, readonly) RACSignal *enabled;

@property (atomic, assign) BOOL allowsConcurrentExecution;

顾名思义,就不过多解释了。

最后说一下,上面的例子只是展示了RACCommand类的使用方法与优点,谈不上多高端,因为对几个属性信号订阅的回调block中都引用了外部对象,也就是会引起“副作用”,如果能尽量避免就会更加符合函数式编程的思想。

最后,再来看一眼文章开头出现过的图片,有没有感觉融会贯通?

参考链接

官方文档:

DesignGuidelines
FrameworkOverview

冷热信号:

细说ReactiveCocoa的冷信号与热信号(一)
细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号
细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号
Comparing replay, replayLast, and replayLazily

命令:

ReactiveCocoa Essentials: Understanding and Using RACCommand

我之前写的:

ReactiveCocoaDemo
RxSwift基本概念与使用
RxSwift进阶,细节与UI绑定
ReactiveObjC基本概念与简单使用