走进ReactiveCocoa的世界

在学习ReactiveCocoa之前,先学习一下概念

ReactiveCocoa 是一套开源的基于Cocoa的FRP框架 .FRP的全称是Functional Reactive Programming,中文译作函数式响应式编程,是RP(Reactive Programm,响应式编程)的FP(Functional Programming,函数式编程)实现。说起来很拗口。太多的细节不多讨论,我们先关注下FRP的FP特征。

函数式编程

函数式编程,简单来说,就是多使用匿名函数,将逻辑处理过程,以一系列嵌套的函数调用来实现,以减少中间状态的存在。

简单举例,一个简单的表达式,(将表达式理解成一系列的逻辑处理流程):

(1 + 2) * 3 - 4
  • 1
  • 2

如果是传统的过程式编程,则会这样写:

a = 1 + 2;
b = a * 3;
c = b - 4;
  • 1
  • 2
  • 3
  • 4

而在函数式编程中,我们将运算过程(逻辑处理流程),定义为不同的函数,然后会写成:

result = subtract(multiply(add(1,2),3),4)
  • 1
  • 2

从这里,我们就可以看出一个特点,过程式编程会在运行中,将一步步操作的结果以状态的形式纪录下来,下一步操作是修改上一步操作的结果。 而在函数式编程中,上一步操作的结果会直接以参数的形式或者其它形式传递给下一步操作,不再本地保存一堆无用的中间状态,而是输入一个初始值,就返回一个相应的结果。中间状态会互相影响,过多的中间状态会降低代码可读性以及提高维护的难度。通过函数式编程,减少状态的存在,一个操作,一个流程,只由输入值来决定输出结果,不在运行过程中以来全局状态或者保存中间状态。 所以函数式编程的主要优点就在于 不保存中间状态,缺点的话,为了不保存这个中间状态,而在函数间传递,会增加函数的调用次数,而这样会在一定程度上降低效率。

函数式编程的其它优点:

  1. 代码简洁,开发快速。
  2. 接近自然语言,易于理解。
  3. 利于单元测试,和模块化组合。
  4. 易于并发编程

函数式编程,指尽量减少状态的保存,直接由输入得到结果,而不是在一些地方放置一堆的状态.即Model更新时,是直接作用于View,让View做出相应的显示,而不是保存一个状态,然后再通知View来获取这个状态.对于少量的状态,这样处理起来可能没问题,但是一旦状态多起来,管理就变得十分麻烦,难以调试.所以函数式编程,目的是 让相同的输入导出相同的输出,减少由于保存状态带来的影响.

响应式编程

响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。 
例如,在命令式编程环境中,a:=b+c表示将表达式的结果赋给a,而之后改变b或c的值不会影响a。但在响应式编程中,a的值会随着b或c的更新而更新。

响应式编程对应的是命令式编程,命令式编程中,数据以状态的形式保存,通过命令来通知需要状态的对象来更新状态。iOS和UIKit在设计上是命令式的,如TableView的DataSource,委托模式强制将状态保存在委托中,以在请求发生时,为TableView提供数据.

使用ReactiveCocoa的原因

数据的改变导致View的改变,现在我们常用的一般是命令式的编程,控制器去发动数据的改变,然后保存数据的状态,然后再去驱动View来加载最新的数据状态.

随着业务变得更加复杂,这种做法导致Controller上有一堆的状态数据,而这些状态数据错综复杂,互相影响,导致代码可读性较差,所以我们倾向于使用一种函数式编程的方式.数据修改后,直接设置View,而不是保存数据的状态,或者说将这步数据状态保存的操作抽离出来,不放在业务代码中,以提升代码的可读性,这就是使用ReactiveCocoa的原因.

Introduction

ReactiveCocoa 是一种Funcational Reactive Programming。 不是用一堆表示状态的变量,这些变量会在许多情况下被修改,而是使用一种事件流的方式,用Signal信号来表示状态,实时传递状态。

事件流已经统一了Cocoa中的常用的异步和实现处理,如:

  • Delegate methods
  • Callback blocks
  • NSNotifications
  • Control actions and responder chain events
  • Futures and promises
  • Key-value observing (KVO)

将这些事件处理统一成一种信号的形式,所以就可以声明链式处理和聚合信号。

总结,很多iOS程序,对于事件的处理和响应是基于应用状态的,随着回调的增加和状态变量的增加,处理这些事件的代码就回变得异常复杂,这就是我们使用ReactiveCocoa的原因,通过响应式的处理以消除中间状态,增加代码可读性和扩展性,降低复杂度。

调试地狱

RAC通过一堆Block来实现事件流的传递,所以调试是意见很可怕的事情,在信号处理中的断点,会发现整个堆栈上全是RAC自己处理信号发送信号的操作,很难找到有用的信息,找到当前断点的上一级触发者。

RAC中推荐通过在事件流中加入副作用来进行调试。.on(event: { print ($0) }),也可以通过logEvents来输出事件流,但是这是2.5以后添加的新接口,而我们当前使用的依旧是2.5版本,因为2.5版本是纯Obejctive-C的。

ReactiveCocoa与 RxSwift.

ReactiveCocoa是由Reactive Extensions(微软的Rx库)启发,并在之后也深深地受其影响的。而RxSwift是RX的正式成员,

ReactiveCocoa是受FRP启发,但开发环境是在Cocoa上,所以API更接近于Cocoa。

主要区别在于RAC中有冷热信号的概念,这是RAC中的一个核心特点,后面会介绍到。RX则把冷热信号统一。

目前我们主要是在OC上开发,以后OC和Swift混编,这些情况下使用RAC都会更加方便一点。

Overview

在RAC的世界中,使用信号流处理事件. 信号的发送者,称为 receiver,信号的接收者,称为subscriber.

Streams

Stream,由RACStream抽象类表示,表示一个对象的一系列的值。

值可以立刻被拿到,也可以在未来被拿到,但收到值是顺序的。不可能在流上不接受到第一个值而直接获取第二个值。

Stream是monads的(在FP中表示用步骤流程表示的运算操作的结构),基于简单的基础信号进行复杂的操作。

RACStream是一个抽象基类,通过signalsequences来实现。

Signals

信号由RACSignal类表示。信号表示那些将要在未来被传递的数据。程序运行时,数据的值在信号中被传递,推送给Subscribers。用户需要去订阅这些信号获取数据。

信号发送三种不同的事件:

  • next : 传递数据流中的新的值。RACStream的操作方法只能处理next类型的事件。传递的数据可以为nil。
  • error : 表示信号完成前发生了错误。事件包含一个NSError的对象来表示具体错误。必须特别处理,因为错误并不包含在RACStream中。
  • completed : 表示信号完成,不会再有新的值被添加到Stream中。也需要特别处理,事件不会被包含在Stream中。

next的事件可以有任意数量个,但是errorcompleted事件最多只会发生一个。

Subscription

subscriber:订阅者,订阅信号,等待处理信号所发送的事件。在RAC中,一个订阅者者指继承RACSubscriber协议。

一个订阅的创建可以通过调用-subscribeNext:error:completed:等方法。订阅者持有所订阅的信号,会在信号完成或出错的时候自动释放,当然也可以手动释放Subscription

Subjects

Subject 用RACSubject类表示,是一个可以手动管理的信号,也就是我们要着重讨论的热信号。

Subject可以理解为一个信号的mutable版本。

不在block中处理应用逻辑,而是将这些block发送给一个共享的Subject来处理。RACSubjectRACSignal的子类。

RACReplaySubject可以缓存Event,供以后的订阅者进行监听。

Commands

RACCommands类,创建和订阅一个信号,并监控其状态。是对信号的封装,将一个信号的状态,以executionSignals表示封装的信号,用executing表示信号执行的状态,用enabled表示信号是否可用,用errors表示信号执行中的异常。

这个属性通常与UI控件结合在一起,用enabled信号来控制控件是否可用,用executionSignals来表示控件可用时要执行的操作。

Connections

RACMulticastConnect类,表示可以在任意数量的订阅者中共享的一个信号。即是我们重点要讨论的第二个问题,冷热信号。

信号默认是冷的,即每当一个新的订阅者添加的时候,他们才开始处理事件,发送信号。这是一件可取的做法,数据会在每次订阅的时候刷新。但是对于有副作用的信号,或者操作消耗太多资源的信号(如网络请求),显然是有问题的。尤其是在RAC中,每次都信号进行的逻辑处理操作都是在订阅前一个信号。

通过RACSignal上的pulish或者multicast:方法创建一个这样的热信号。

Sequences

RACSequence表示表示一组信号,类似于NSArrayRACSequence表示一些列的信号,其有两个主要属性,id类型的head,和RACSequence类型的tail,则遍历这个列表时类似于一种递归的方式。则就体现了RACSequence的懒加载特性,如果这组Sequences中的值没有被使用,那就不会去获取这个值。

一般用于遍历数组 :

NSArray *numbers = @[@(1),@(2),@(3),@(4),@(5)];
    NSArray *result = [[[[numbers rac_sequence] 
      filter:^BOOL(NSNumber *value) {  
         return [value intValue] %2 ==0;  
      }] map:^id(NSNumber *value) {    
         long square = [value intValue] * [value intValue];
         return @(square); 
      }] array];       
NSLog(@"results = %@",result);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Disposables

RACDisposables表示对信号的取消操作和资源的释放操作。用于取消信号的订阅,释放信号。一般网络操作和后台处理的一些耗时操作,都应该提供RACDisposables。调用RACDisposablesdispose方法,以取消正在订阅中的信号. 而当信号执行完成时,也会调用RACDisposables.

Schedulers

RACScheduler,提供一系列的执行队列,供信号按需执行操作或者发送结果。

RACScheduler类似于GCD,但是提供了取消功能,通过disposables,而且只执行串行任务。对于使用immediateScheduler创建的scheduler,不支持使用同步方法。可以看到设计是在通过一些限制来避免死锁的发生。

Value types

RAC提供了一些类在Stream中传递值。

  • RACTuple : 一个简单的固定大小的集合,可以包含nil对象(用RACTupleNil对象表示)。一般用于表示多个信号聚合时,聚合信号传递的数据的值。
  • RACUnit : 代表一个空值对象。
  • RACEvent : 代表信号的事件,即next,error,completed三种事件。通过materialize方法,将三种信号合成一种发送给订阅者来统一处理。

Basic Operators

介绍几个简单的基础的操作符.

Performing side effects with signals

Subscription

使用 subscribe命令来根据信号的当前或未来的值设定响应操作:

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

// Outputs: A B C D E F G H I
[letters subscribeNext:^(NSString *x) {
    NSLog(@"%@",x);
}];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在冷信号中,每次订阅信号都会执行副作用.

Injecting effects

使用 do...命令,在不订阅信号的情况下,添加副作用.

__block unsigned subscriptions = 0;

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

// Does not output anything yet
loggingSignal = [loggingSignal doCompleted:^{
    NSLog(@"about to complete subscription %u",subscriptions);
}];

// Outputs:
// about to complete subscription 1
// subscription 1
[loggingSignal subscribeCompleted:^{
    NSLog(@"subscription %u",subscriptions);
}];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

do的操作会比 subscribe要先执行.

Transforming streams

这些操作,将一个信号流转变为一个新的信号流.

Mapping

使用map:命令,将信号的值进行替换.

// 以" "为间隔符创建sequence
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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Filtering

使用filter:命令,过滤信号值,过滤NO的信号值:

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Combining streams

将多个信号流聚合成一个信号流

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
  • 2
  • 3
  • 4
  • 5
  • 6

Flattening

使用flatten:命令,对于信号中的信号,将 信号的值整合进一个新的信号流. 如下,连接Sequence时:

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];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里 sequenceOfSequences,这个信号的值 是一个信号,使用flatten,将信号中的信号的值 给取出来,作为信号的值,创建一个新的信号.

再聚一个聚合信号的例子:

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

Mapping and flattening

使用flatten的目的,一般并不是为了这个效果,而是为了操作flattenMap:.

flattenMap:命令,用于转换多个信号流中的信号值输出一个新的信号流.从字面意思上来解释,这个操作就是,先map操作,处理信号,然后再flatten操作,将信号中的信号提取出来作为一个完整的信号. 也就是说,block中return的是一个信号,而flattenMap返回值是一个信号,将return中的信号flatten后的一个完整的信号.还是用sequence来举例:

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]; 
    }
}];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

flattenMap也用于将复数个信号的工作自动的结合在一起:

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Combining signals

将多个信号聚合成一个信号.

Sequencing

使用then:命令,一般为 signalA then:^{return signalB},表示 订阅signalA的信号,但是忽略所有的next事件,当completed事件发送时,订阅B信号并返回B信号的事件.

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

使用场景,执行完前一个信号的所有副作用,然后开始另一个新信号,将其返回值作为真正的信号值 传递.

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Combining lastest values

combineLastest:combineLastest:reduce:两个方法,用来聚合信号,当多个信号中的任何一个信号发生变化时,都会取每个信号的最新值,组合成一个新的值发送.而两个方法的区别在于,前者传递的信号是一个RACTuple对象,将多个信号的值封装在一个对象中,而后者在reduce的block中来处理多个信号的值,将其整合成一个值返回.

RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *combined = [RACSignal
    combineLatest:@[ letters,numbers ]
    // reduce的block的参数可以自行添加,但顺序要与combineLatest中信号的顺序相同.
    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"];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

一定要注意这里,聚合的所有信号都有第一个值后,这个聚合信号才会发送第一个值,所以这里letters发送的第一个信号A,不会在聚合信号combined中出现.

然后就要与另外一个方法zip进行比较.

zip stream

压缩信号.与上面的combineLastest有些相似,但是区别在于, zip需要等待多个信号都有一个最新的值后,才会发送一个信号,而combine中,任何一个信号有新的值后,都会发送信号.

举例,还是类似combineLatest中的例子,只是将combineLatest改为zip,但是结果就变了.

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

// Outputs: A1 B2 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"];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在这里,最终输出地结果是A1,B2,C3.

Switching

使用switchToLastest来获取信号中的信号的值,传递nexterror,但是不传递complete:

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"];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

当信号中的信号发送complete时,由于switchToLastest不接收这个completed事件,所以 switchToLastest的信号依旧继续处理.但是当信号中的信号发送error时,会中断switchToLastest的信号处理:

RACSubject *signalOfSignals = [RACSubject subject];
RACSignal *switched = [signalOfSignals switchToLatest];

[switched subscribeNext:^(id x) {
    NSLog(@"Next : %@",x);
} error:^(NSError *error) {
    NSLog(@"Error : %@",error);
} completed:^{
    NSLog(@"Completed!");
}];

RACSubject *signalA = [RACSubject subject];
[signalOfSignals sendNext:signalA];
[signalA sendNext:@"A"];
[signalA sendCompleted];
[signalA sendNext:@"AA"];

RACSubject *signalB = [RACSubject subject];
[signalOfSignals sendNext:signalB];
[signalB sendNext:@"B"];
[signalB sendError:[NSError errorWithDomain:@"error" code:1 userInfo:nil]];
[signalB sendNext:@"BB"];

RACSubject *signalC = [RACSubject subject];
[signalOfSignals sendNext:signalC];
[signalC sendNext:@"C"];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

上面这段代码示例中,输出结果为 :

Next : A
Next : B
Error : Error Domain=error Code=1 "(null)"
  • 1
  • 2
  • 3
  • 4

信号A发送完成后,AA的信号就无法发送给signalOfSignals了,但是一个新的信号B还是能够继续发信号给signalOfSignals的.但当信号B发送error后,这个signalOfSignals会接受到这个error,从而结束订阅.

Design Guidelines

关于RACSequence的一些特性

延迟加载

RACSequence中获取值,默认是延迟计算的:

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

只有真正使用时,才会进行计算,获取到真正的值;如通过sequence.head才会去计算出A_的值。

而且,只会计算一次,即多次访问sequence.head,但[str stringByAppendingString:@"_"]操作只会执行一次。

如果不需要这种延迟加载,而需要在以来开始的时候初始化整个数组,那就使用eagerSequence这个属性。

计算操作是同步执行的,这需要注意一下。如果数组的计算操作是比较耗费时间的,可以通过接口signalWithScheduler:来在一个队列中执行数组计算操作,并获取完成信号。

副作用只执行一次

对于RACSequence进行运算时,因为所做的运算一般是求得一个新的RACSequence,而RACSequence只会在第一次使用到值时才会进行计算:

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

// Logs "A" during this call. 进行计算,执行block中方法,所以有log
NSString *concatA = sequence.head;

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

// Does not log anything. 已经完成计算,不会再输出log。
NSString *concatB2 = sequence.tail.head;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

关于RACSignal一些需要注意的地方

Signal events are serialized

信号事件是连续的串行的。一个信号可以分发事件到任何一个线程,连续的事件可以被选择在不同的线程或者schedulers.但是有些情况会要求事件必须在特殊的scheduler上执行,如UI操作,就必须在主线程上进行。

RAC保证,不会有两个信号同时到达,所以不会有两个信号的事件同时在一个线程上被激活的可能。即在RAC中,当一个事件在处理过程中,不会有其他事件被分发。只有当事件被处理完成后才会有新的事件发送出去。

这就意味着 : 在-subscribeNext:error:completed:回调中,不需要对变量进行加锁操作,因为事件处理全部是串行的。

Subscription will always occur on a scheduler

订阅操作,始终执行在一个schedular上。为了确保createSignalsubscribe方法执行的一致性表现,RAC会保证 消息操作的执行和 消息订阅的操作会在同一个scheduler中。

如果在订阅时,执行代码+[RACScheduler currentScheduler]无法获得一个RACScheduler时,会将订阅和消息放在一个后台的RACScheduler中执行。如果能够得到一个RACScheduler则会在当前RACScheduler中执行。

再说明一下这个[RACScheduler currentScheduler],这个函数会再 RACScheduler中返回正确的Scheduler,以及在主线程中返回+[RACScheduler mainThreadScheduler]. 所以上面一段话的意思,就是如果在主线程或者在执行一个RACScheduler中,订阅会发生在这个Scheduler中,否则会发生在一个后台的Scheduler中。

Errors are propagated immediately

在RAC中,error在语义上表示异常。当信号中发生一个Error信号时,会立即发送给所有相关的信号,并使整个消息链终止。

但这并表示 ,对于Error的处理的操作符 ,如catch: catchTo: 和materialize 这几个错误处理也会终止。

Side effects occur for each subscription

每一次对信号的订阅,都会触发副作用。原因很简单,因为这些信号是冷信号,冷信号会在订阅时执行。而需要注意的是,所有对信号的操作,都是订阅信号,并发送新的信号。

想要取消这种效果,那就是用热信号吧。

再次说明一下,冷信号的副作用效果,会产生许多问题,且难以发现,一定要注意。要理解冷热信号的区别。

Subscriptions are automatically disposed upon completion or error

当一个消息发送了completederror事件时,这个subscription会自动被释放。节省手动释放的操作。

而释放信号时,要对那些 文件操作或者网络操作等,进行资源释放和过程中断。

Best practices

Use descriptive declarations for methods and properties that return a signal

当一个方法或者属性返回一个RACSignal类型的信号时,很难很快地理解一个信号的含义。

对于声明一个信号,有以下三个关键性的问题:

  1. 信号是热信号还是冷信号?
  2. 信号有一个值还是没有值还是多个值?
  3. 信号有副作用吗?

热信号且没有副作用 ,这种情况应该将信号作为一种属性。使用属性,表明对信号的订阅不需要进行初始化,而且添加新的订阅也不会改变这个用法。信号的属性一般被命名为 名字 + 事件 ,如 textChanged.

冷信号且无副作用 ,这种情况应该作为一个函数,且命名使用一个名词来表示,如currentText. 一个名词的函数声明,表示了这个信号不会被一直持有,同时声明操作是发生在订阅时.如果信号发送了复数个值,需要在命名时表明这一点,如currentModels

有副作用的信号,信号应该是以方法形式返回,并表示动作,如logIn. 动词表明了这个函数不是静态的,调用者要小心调用时的副作用. 如果信号会发送一个或者多值,应该要再命名中表明值的含义,如loadConfigurationfetchLastestEvents.

Indent stream operations consistently

使用RAC书写代码时,在处理信号中得操作流很容易变得很重很多,大量的操作符与block聚集在一起,如果没有进行很好地格式化,那这段代码就将变得乱七八糟.所以,建议,在流的处理过程中,对操作符进行缩进 :

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);
    }];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Use the same type for all the values of a stream

在一个流中,使用一种类型来作为各个过程的信号值.虽然RAC中支持使用任何类型的值作为信号值来传递,但是在一个完整地流中,使用多种不同类型的值,会导致代码可读性降低,也会增加订阅者的负担,必须更加小心地去处理这个奇怪的信号.

Avoid retaining streams for too long

不要持有RACStream对象过长时间.持有一个RACStream对象的同时,也会导致以来这个RACStream对象的所有对象都被持有,无法正常释放,这将降低内存利用率.

例如 :一个RACSequence对象在需要使用其head属性时,可以持有这个对象,但当不再使用head时,就应该抛弃这个RACSequence了,如果需要之后的数据,可以持有其tail属性而不是持有这个RACSequence本身.

Process only as much of a stream as needed

保持一个Stream或者RACSignal的订阅,会浪费性能和内存,如果一个信号的结果不需要使用,就应该丢弃这些信号.

我们可以使用take:takeUntil:等方法,来做这种判断逻辑.这个方法会在逻辑判断不会在接收消息时,取消该信号订阅的堆栈,终止所有的依赖项的订阅.

Deliver signal events onto a known scheduler

可以将信号使用deliverOn:在一个指定的Scheduler上发送事件,如对于一些UI的操作,可以声明其在主线程中执行. 这个命令指的是subscribe的操作在指定的Scheduler中执行,但是副作用还是在原始的线程中执行.

但是,尽量少得去切换Scheduler,线程间的切换,会有不必要的延迟出现,而且会消耗CPU的性能. 所以deliverOn:的操作,一般放在信号链的最后一级执行.

Make the side effects of a signal explicit

明确地说明一个信号有副作用. 我们应该避免信号的副作用,因为我们很难控制副作用的发生.

但这种场景还是需要的,所以RAC中提供了doNext: doError:doCompleted三个方法来提供明确地副作用的处理.

Share the side effects of a signal by multicasting

在热信号中,分享副作用.使用publishmulticast两个命令来让一个信号发布成一个热信号,变成RACMulticastConnection对象.

Debug streams by giving them names

每个RACStream都有一个属性name,来用于调试.而一个Streamdescription的中会自动包含所有操作的列举出来.

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

NSLog(@"%@",signal);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如上面打印出来的结果是 :[[[RACObserve(self,username)] -distinctUntilChanged] -take: 3] -filter:

可以通过setNameWithFormat来设置一个signal最开始的名称.RACSignal也提供了logNext,logError,logCompletedlogAll这些方法,可以自动的在事件发生时打日志.

Avoid explicit subscriptions and disposal

避免明确地 订阅和释放操作.而使用以下几个方法:

  • 使用RAC()RACChannelTo()宏,来绑定一个信号到对应的属性,而不是声明一个手动的改动来执行操作.
  • 使用rac_liftSelector:withSignals:方法,当信号发生时会调用Selector.
  • takeUntil:这样的方法,用来自动的释放一次订阅.

尽量使用RAC提供的方法来操作信号来导出一个正确的符合效果的信号流,供订阅.

Avoid using subjects when possible

避免使用SubjectsSubjects是一个强力的工具,桥接代码与信号.但是过度使用会导致代码变得更加复杂.尽量少的使用热信号,建议:

  • 对于要给信号中得值初始化的情况,使用createSignal:的block进行初始化操作.
  • 对于分发一个中间的结果给subject的情况,改用 combineLatest:zip:方法来合并多个信号来实现.
  • 对于想要做出一个热信号供多个对象订阅的情况,通过multicast一个基础的信号来解决.
  • instead of implementing an action method which simply controls a subject,use a RACCommand or rac_signalForSelector instead.

如果要使用subjects,应该将其作为一个信号链的基础,而不是中间的一个环节.

Memory Management

RAC中得内存管理是很复杂的,但是这样做的目的是,使用者不用持有信号来驱动信号发送的过程.

除了那些会长期存在的信号,会被以属性的形式持有,一般不要去持有信号.

  1. 信号的创建后,会自动的添加到一个全局的信号集合中.
  2. 然后这个信号会等待一次 main run loop的执行,这时如果信号还是没有被订阅,那信号将被集合移除.如果信号没有在其他地方被持有,那信号将会在这个时候被释放.
  3. 如果信号被订阅了,那信号会在这个信号集合中被持有.
  4. 如果信号的所有订阅者都消失了,那会重复第二步操作,即再等待一个runloop周期.

Subscribers

当使用subscribeNext:error:completed:订阅信号时,隐式地创建了一个RACSubscriber对象.所以创建信号时使用的block所关联的对象会被订阅所持有.

Finite or Shore-Lived Signals

在RAC的内存关联中,一个重要的注意事项就是,订阅会在 completion 或是error时终止,订阅者也会被移除.

这样,信号的生命周期也就会跟随事件流的逻辑生命周期.

Infinite Signals

会有一些不会自行结束的信号存在,所以需要disposable存在.

信号订阅的dispose操作,会移除所有关联的订阅者,而且也会释放该信号所占有的资源.

Signlas Derived from self

有些信号是有self衍生出来的.如 RACObserve()监听self的一个属性时,在subscribeNext使用self指针,就会形成一个引用环.

建议使用@weakify@strongify这两个宏来处理指针. 当对象不能使用weak时,使用 __unsafe_unretained 或 @unsafeify.

但很多时候,有一种更好地写法来解决循环指针的问题,如对于一般写法:

@weakify(self);
[RACObserve(self,username) subscribeNext:^(NSString *username) {
    @strongify(self);
    [self validateUsername];
}];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

实际上我们可以这样写:

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

或者这样写 :

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

冷热信号详解

这里的内容主要是在学习 美团的技术分享文章,里面几张图也是直接从这里拿过来的.

然后我们再来讨论这个RAC中一个重点问题.也就是冷热信号.

冷热信号的起源来自于RX的Hot ObservableCold Observable,两者的区别是:

  • 热信号是主动的,即使你没有订阅事件,它仍然会时刻推送。
  • 热信号可以有多个订阅者,是一对多,信号可以与订阅者共享信息

产生热信号的原因,有些信号我们不想再订阅时就执行一次,而是全局共享一个信号.如网络请求的信号,我们并不希望在每次对网络请求结果信号进行订阅时,就执行一次新的网络请求.所以我们要将信号转换为热信号,以只执行一次网络访问,而结果可以供多次订阅共享.

热信号都属于一个类RACSubject,这个类在RAC中表示一个可变的信号.我们写一段代码来演示一下其效果:

RACSubject *subject = [RACSubject subject];
RACSubject *replaySubject = [RACReplaySubject subject];

[[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{
    // Subscriber 1
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscriber 1 get a next value: %@ from subject",x);
    }];
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscriber 1 get a next value: %@ from replay subject",x);
    }];

    // Subscriber 2
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscriber 2 get a next value: %@ from subject",x);
    }];
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscriber 2 get a next value: %@ from replay subject",x);
    }];
}];

[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{
    [subject sendNext:@"send package 1"];
    [replaySubject sendNext:@"send package 1"];
}];

[[RACScheduler mainThreadScheduler] afterDelay:1.1 schedule:^{
    // Subscriber 3
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscriber 3 get a next value: %@ from subject",x);
    }];
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscriber 3 get a next value: %@ from replay subject",x);
    }];

    // Subscriber 4
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscriber 4 get a next value: %@ from subject",x);
    }];
    [replaySubject subscribeNext:^(id x) {
        NSLog(@"Subscriber 4 get a next value: %@ from replay subject",x);
    }];
}];

[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
    [subject sendNext:@"send package 2"];
    [replaySubject sendNext:@"send package 2"];
}];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

输出的结果是 :

2016-06-30 23:34:25.722 TestPods[17259:2788197] Subscriber 1 get a next value: send package 1 from subject
2016-06-30 23:34:25.723 TestPods[17259:2788197] Subscriber 2 get a next value: send package 1 from subject
2016-06-30 23:34:25.723 TestPods[17259:2788197] Subscriber 1 get a next value: send package 1 from replay subject
2016-06-30 23:34:25.724 TestPods[17259:2788197] Subscriber 2 get a next value: send package 1 from replay subject
2016-06-30 23:34:25.834 TestPods[17259:2788197] Subscriber 3 get a next value: send package 1 from replay subject
2016-06-30 23:34:25.834 TestPods[17259:2788197] Subscriber 4 get a next value: send package 1 from replay subject
2016-06-30 23:34:26.818 TestPods[17259:2788197] Subscriber 1 get a next value: send package 2 from subject
2016-06-30 23:34:26.818 TestPods[17259:2788197] Subscriber 2 get a next value: send package 2 from subject
2016-06-30 23:34:26.818 TestPods[17259:2788197] Subscriber 3 get a next value: send package 2 from subject
2016-06-30 23:34:26.819 TestPods[17259:2788197] Subscriber 4 get a next value: send package 2 from subject
2016-06-30 23:34:26.819 TestPods[17259:2788197] Subscriber 1 get a next value: send package 2 from replay subject
2016-06-30 23:34:26.819 TestPods[17259:2788197] Subscriber 2 get a next value: send package 2 from replay subject
2016-06-30 23:34:26.820 TestPods[17259:2788197] Subscriber 3 get a next value: send package 2 from replay subject
2016-06-30 23:34:26.820 TestPods[17259:2788197] Subscriber 4 get a next value: send package 2 from replay subject
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

根据时间线画图如下:

分享图片

而如果是冷信号的情况的话,就有如下时间线:

分享图片

可以发现,对于冷信号,类似于重播,每个订阅者都会观察到整个消息的处理过程.而对于subject,类似于直播,多个订阅者接收到一个信号的事件,并且如果信号已经发送过的消息,错过就无法再次接收到.

而还有一个replaySubject对象,将上面代码改写为该对象后,得到的效果图如下:

分享图片

发现这个信号会保存之前发送过的信号,在新的对象订阅时,将之前的信号发送.

将一个冷信号变成热信号

RACSubject是支持RACSubscriber协议的,热信号的实现就是通过这个RACSubject来订阅一个冷信号,然后其他人在再来订阅这个RACSubject.

观察以下代码:

RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    NSLog(@"Cold signal be subscribed.");
    [[RACScheduler mainThreadScheduler] afterDelay:1.5 schedule:^{
        [subscriber sendNext:@"A"];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{
        [subscriber sendNext:@"B"];
    }];

    [[RACScheduler mainThreadScheduler] afterDelay:5 schedule:^{
        [subscriber sendCompleted];
    }];

    return nil;
}];

RACSubject *subject = [RACSubject subject];
NSLog(@"Subject created.");

[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{
    [coldSignal subscribe:subject];
}];

[subject subscribeNext:^(id x) {
    NSLog(@"Subscriber 1 recieve value:%@.",x);
}];

[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{
    [subject subscribeNext:^(id x) {
        NSLog(@"Subscriber 2 recieve value:%@.",x);
    }];
}];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

输出结果为 :

2016-07-01 16:32:04.829 TestPods[17618:2842318] Subject created.
2016-07-01 16:32:07.029 TestPods[17618:2842318] Cold signal be subscribed.
2016-07-01 16:32:08.669 TestPods[17618:2842318] Subscriber 1 recieve value:A.
2016-07-01 16:32:10.319 TestPods[17618:2842318] Subscriber 1 recieve value:B.
2016-07-01 16:32:10.319 TestPods[17618:2842318] Subscriber 2 recieve value:B.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

得到下图:

分享图片

这样自行处理热信号,过于简单,有一些问题,如当Subject取消订阅时,不能取消对应的冷信号的订阅.而RAC中有对冷信号转换为热信号的标准接口 :

//创建一个普通的热信号
-(RACMulticastConnection *)publish;
// 创建一个热信号,并将值发送给一个RACSubject对象
-(RACMulticastConnection *)multicast:(RACSubject *)subject;
// 创建重播热信号,并立即订阅,信号使用RACReplaySubject,即会重播已经发送的所有信号
-(RACSignal *)replay;
// 创建一个热信号,使用RACReplaySubject,但设置capacity为1,即只会重播一次信号
-(RACSignal *)replayLast;
// 创建一个热信号.但不立即订阅,等待其他人订阅这个热信号.
-(RACSignal *)replayLazily;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

RAC提供的热信号处理的具体实现

RAC提供以上五个方法中,最重要的就是 - (RACMulticastConnection *)multicast:(RACSubject *)subject,其他都是基于这个实现的.看一下这个函数的实现:

/// implementation RACSignal (Operations)
-(RACMulticastConnection *)multicast:(RACSubject *)subject {
    [subject setNameWithFormat:@"[%@] -multicast: %@",self.name,subject.name];
    RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject];
    return connection;
}

/// implementation RACMulticastConnection

-(id)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject {
    NSCParameterAssert(source != nil);
    NSCParameterAssert(subject != nil);

    self = [super init];
    if (self == nil) return nil;

    _sourceSignal = source;
    _serialDisposable = [[RACSerialDisposable alloc] init];
    _signal = subject;

    return self;
}

#pragma mark Connecting

-(RACDisposable *)connect {
    BOOL shouldConnect = OSAtomicCompareAndSwap32Barrier(0,1,&_hasConnected);

    if (shouldConnect) {
        self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];
    }

    return self.serialDisposable;
}

-(RACSignal *)autoconnect {
    __block volatile int32_t subscriberCount = 0;

    return [[RACSignal
        createSignal:^(id<RACSubscriber> subscriber) {
            OSAtomicIncrement32Barrier(&subscriberCount);

            RACDisposable *subscriptionDisposable = [self.signal subscribe:subscriber];
            RACDisposable *connectionDisposable = [self connect];

            return [RACDisposable disposableWithBlock:^{
                [subscriptionDisposable dispose];

                if (OSAtomicDecrement32Barrier(&subscriberCount) == 0) {
                    [connectionDisposable dispose];
                }
            }];
        }]
        setNameWithFormat:@"[%@] -autoconnect",self.signal.name];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

简单说明一下流程:

  1. multicast:方法将 signalsubject作为参数创建一个RACMulticastConnect的热信号.
  2. RACMulticastConnection的 initWithSourceSignal: subject :初始化时,创建一个RACSerialDisposable对象用于取消订阅.
  3. 在对RACMulticastConnection对象调用connect时,会判断热信号是否已经与原始信号连接在一起了,如果没有的话,则用_signal这个对象订阅sourceSignal.
  4. 而这个_signal是一个RACSubject的对象,所以是一个热信号,会在connect时订阅sourceSignal,然后传递事件.

然后再来看一下 另外4个方法的实现:

/// implementation RACSignal (Operations)
-(RACMulticastConnection *)publish {
    RACSubject *subject = [[RACSubject subject] setNameWithFormat:@"[%@] -publish",self.name];
    RACMulticastConnection *connection = [self multicast:subject];
    return connection;
}

-(RACSignal *)replay {
    RACReplaySubject *subject = [[RACReplaySubject subject] setNameWithFormat:@"[%@] -replay",self.name];

    RACMulticastConnection *connection = [self multicast:subject];
    [connection connect];

    return connection.signal;
}

-(RACSignal *)replayLast {
    RACReplaySubject *subject = [[RACReplaySubject replaySubjectWithCapacity:1] setNameWithFormat:@"[%@] -replayLast",self.name];

    RACMulticastConnection *connection = [self multicast:subject];
    [connection connect];

    return connection.signal;
}

-(RACSignal *)replayLazily {
    RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]];
    return [[RACSignal
        defer:^{
            [connection connect];
            return connection.signal;
        }]
        setNameWithFormat:@"[%@] -replayLazily",self.name];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  1. 对于publish,创建一个普通的RACSubject对象,一个普通的热信号.
  2. 对于replay ,创建一个RACReplySubject对象的热信号,这个热信号会重播之前的历史信号值.
  3. 对于replayLast ,以RACReplySubject对象创建一个热信号,但是设置Capacity为1,也就是只重发最后一次的历史值.
  4. 对于replayLazily ,使用defer命令,只在信号真正被订阅时,才去连接热信号.

RACCommand

RACCommand对信号进行封装,是信号在某些事件发生时触发,一般与UI操作结合.RACCommand是对信号的一层非常漂亮的封装,用事件来触发信号执行的设计,使RACCommand适用于许多情况,不仅仅是UI操作,其内部对信号热化,以及监控信号状态,可以用于一些耗时操作的信号化,如网络请求.

我们首先来看一下,RACCommand提供的接口:

executionSignals

在调用execute:后,一个返回信号的信号. 这个信号是一个信号的信号,封装了workSignal(本文之后用workSignal这个词来形容用户于RACCommand中要执行的信号).当receiverenable的时候,发送信号.RACCommand将正在执行的信号封装在这里,作为executionSignals的返回值,而信号中的error被发往RACCommanderrors的信号中了,而遇到error信号时,executionSignals的信号会返回一个completed信号以标记事件完成:

RACCommand *comd = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:[NSString stringWithFormat:@"INPUT :%@",input]];
        [subscriber sendError:[NSError errorWithDomain:@"ddd" code:1 userInfo:nil]];
        return nil;
    }];
}];

[[comd.executionSignals switchToLatest]
 subscribeNext:^(id x) {
     NSLog(@"Next : %@",x);
 } error:^(NSError *error) {
     NSLog(@"Next : %@",error);
 } completed:^{
     NSLog(@"Completed");
 }];

[comd.executionSignals subscribeNext:^(id x) {
    NSLog(@"Signal %@ ",x);
}];

[comd.errors subscribeNext:^(id x) {
    NSLog(@"error 里才有? %@",x);
}];

[comd execute:@"hello world"];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

如上代码,最终输出为:

2016-07-02 20:39:13.068 TestPods[23173:3420416] Signal <RACDynamicSignal: 0x7ff2d9406d30> name:  
2016-07-02 20:39:13.068 TestPods[23173:3420416] Next : INPUT :hello world
2016-07-02 20:39:13.069 TestPods[23173:3420416] Completed
2016-07-02 20:39:13.069 TestPods[23173:3420416] error 里才有? Error Domain=ddd Code=1 "(null)"
  • 1
  • 2
  • 3
  • 4
  • 5

但是,可以通过materialize方法来获取这个inner errors.

RACCommandexecutionSignals封装workSignal,workSignal的执行必须调用excute来驱动.内部信号的订阅操作会在主线程中执行.

executing

表示当前命令是否正在执行的信号.RACCommandexcute调用后,且在信号终止前,这个信号会发送YES.当信号结束了,会发送NO.

RACCommand中的信号全是热信号,可以随便订阅,且所有订阅结果都在主线程中执行. 这个executing使用的是replayLast,所以订阅时就会获得当前执行的状态.

上面说到,当workSignal失败时,也会发送一个Completed的事件,而这个事件也是executing正确处理信号状态的前提.所以在workSignal一定要正确处理信号的状态,在信号处理完成或者失败的时候,要正确地发送Completed或者error事件.

enabled

决定 workSignal是否可以执行的 信号.

只在两种情况下返回 NO :

  • 这个RACCommand是使用initWithEnabled:signalBlock:初始化,即设置了一个enabledSignal,而且这个信号当前返回NO.
  • allowsConcurrentExecution属性设置为 NO,且这个信号正在执行中.

除了这两种情况,一般都返回YES.

这个信号 一般用于操作UI控件的状态,如UIButton的状态,当未满足某些判断逻辑时,enabled为NO,同时设置UIButton不可点击.

errors

workSignal的错误事件被转发到这里.

注意,这里错误订阅需要订阅Next事件,而不是Error事件,因为在RACSteam中,错误事件的发生会关闭信号流.

allowsConcurrentExecution

workSignal信号是否支持并发.

默认是NO,即RACCommand封装的workSignal同时只能有一个信号正在执行.

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

以一个返回workSignalsignalBlock初始化,没有enableBlock.

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

初始化时,设置 signalBlock,和enabledSignal.

对于enabledSignal,初始状态是YES,即可用.

对于signalBlock,其中返回一个workSignal,这个信号可以带一个输入值,返回的workSignal会以 executionSignals的值的形式被订阅者获取,workSignal会被加热,而executionSignals本身也是一个热信号.

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

RACCommand是enable的时候,调用execute,

  1. 执行signalBlock中的初始化操作,以输入值input来初始化一个新的workSignal
  2. 加热workSignal,使用RACReplaySubject.
  3. 将这个热的workSignalexecutionSignals上发送.
  4. 订阅者通过switchToLatest来获取这个workSignal,并在主线程中订阅事件.

这个函数是有返回值,返回值也是一个RACSignal,返回值信号为 加热后的workSignal.而如果command的enbaled信号为No时,会返回一个 发送一个RACCommandErrorNotEnabled错误的信号.

总结 RACCommand

RACCommand中,封装的所有信号都是 热信号,订阅事件会发生在主线程上.

我们将workSignal考虑为一个任务,而RACCommand为这个任务提供了非常方便的 状态监控,并发控制,参数传递等功能. 我们可以将这样的一个任务以信号的形式融入RAC的世界中,而不用自己去考虑冷热信号,不用去考虑状态控制.RACCommand是我们使用RAC的一个强力而方便的工具.

我们可以将很多任务通过RACCommand进行封装,如网络请求,类似下面这种封装 :

RACCommand *logginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id params) {
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASEURL]
                                                                 sessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
        [manager POST:LOGINURL parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task,id  _Nullable responseObject) {
            [subscriber sendNext:responseObject];
            [subscriber sendCompleted];
        } failure:^(NSURLSessionDataTask * _Nullable task,NSError * _Nonnull error) {
            [subscriber sendError:error];
        }];
        return nil;
    }];
}];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

像这样通过RACCommand封装网络请求,通过excute:传入一次网络请求的参数,伴随着RACCommand完整的热信号保证,和状态控制,使用ReactiveCocoa会变得更加方便高效.

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom