ReactiveObjC基本概念与简单使用

前几个月一直在学习RxSwift,确实相当酷的一个开源库,受益匪浅。在未来学习swift版本(ReactiveSwift)RAC(ReactiveCocoa)之前特意花了3天回顾了一下OC版本(ReactiveObjC)。而之所以愿意写下本篇,是因为这3天中有1天半是在仔细阅读官方文档。官方文档理解之后再去看之前看过的一些他人写的博客,发现质量良莠不齐,真正值得一读的屈指可数。不禁想到原来居然走了那么多弯路。万维刚说:只有学习了“学习的方法”之后才能快速进步。所以学会了哪个开源库不重要,重要的是怎么学会的。越是复杂的开源库,越是要仔细阅读官方文档,之后遇到困惑的地方再找博客对比查证一番,事半功倍。

言归正传,本篇文章主要分为三个部分:ReactiveObjC简介,ReactiveObjC中的基本概念与简单使用,ReactiveObjC中丰富而神奇的操作符。

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

继续阅读之前,强烈建议读者先去了解或者重温一遍官方文档IntroductionDocumentation,对于接下来的理解会很有帮助。

ReactiveObjC简介

ReactiveCocoa-简称为RAC,现在可分为OC版本-ReactiveObjC和swift版本-ReactiveSwift。本篇文章仅介绍ReactiveObjC,之后会有介绍ReactiveSwift的。

RAC是一个将函数响应式编程范式带入iOS的开源库,其兼具函数式与响应式的特性。它是由Josh Abernathy和Justin Spahr-Summers当初在开发GitHub for Mac过程中创造的,灵感来源于Functional Reactive Programming。所以,这么一个神奇伟大的库,竟然是个副产物!而这个副产物比孕育它的产品出名的多,不得不说很有意思。

那么问题来了,什么是函数响应式编程-简称为FRP 呢?一言以蔽之,FRP是基于异步事件流进行编程的一种编程范式。针对离散事件序列进行有效的封装,利用函数式编程的思想,满足响应式编程的需要。

网上资料一大堆,这里就不多介绍了,重点说一下我个人的理解。

函数式编程

举一个简单的🌰:

已知:f(x) = 2sin(x + π/2) + 3, 求 f(π/2)的值。

怎么做呢,把 x = π/2 就可以得出答案,so easy。

那如果是函数式做法呢?

首先定义如下几个函数:

f1(x) = x ;

f2(x) = x + π/2 ;

f3(x) = sin(x) ;

f4(x) = 2x ;

f5(x) = x + 3 ;

然后将最初的f(x)改写成f(x) = f5(f4(f3(f2(f1(x)))))

也就是说,将每一个复杂的问题都设计成一个高阶函数,其中的参数又是一个新的函数,以此类推。有点类似陈凯歌电影《无极》里面的 “圆环套圆环”。

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

上个月看了几页王东岳的《物演通论》,惊为天书,虽然几乎没看懂神马,但是也不是一无所获。“尺度” 是一个看待问题非常重要的点。同一个问题应用不同尺度可能得出的结论天壤之别。
从时间角度看,朝夕是一种尺度,一万年是另一种尺度,几亿年是第三种尺度;从空间角度看,微观是一种尺度,宏观是另一种尺度。

程序员追求将代码写得结构清晰,逻辑合理,很大一部分原因是为了能够高效“复用”。面向对象编程可复用的“尺度”是“类”级别的,而函数式编程可复用的尺度是“函数”级别的。

响应式编程

响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

来看下面一小段代码:

NSInteger a = 3;
NSInteger b = 4;
NSInteger c = a + b;
NSLog(@"c is %ld",c);

a = 5;
b = 6;
NSLog(@"c is %ld",c);

初始化c时其值等于a和b的总和,当a或者b或者a与b同时改变时,若想让c的值仍然等于a和b的总和,若是命令式代码需要重写一遍c = a + b;而若是响应式则完全不需要。

iOS中其实也有响应式编程的典型例子:Autolayout。

举个实际些的🌰:

为了实现在注册页面注册按钮enable状态由几个textField的文本内容决定这么一个小需求。

应用命令式写法类似如下:

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeRegisterButtonEnableState) name:UITextFieldTextDidChangeNotification object:nil];
}
- (void)changeRegisterButtonEnableState {
    self.registerButton.enabled = [self isInfomationValid];
}
- (BOOL)isInfomationValid {
    return self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0 && [self.passwordTextField.text isEqualToString:self.confirmTextField.text];
}

可以看出,此时一个完整的逻辑会被分散到多个方法中,散乱的分布在各个位置。
应用RAC响应式写法类似如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    RACSignal *validSignal = [RACSignal
    combineLatest:@[self.usernameTextField.rac_textSignal,
                    self.passwordTextField.rac_textSignal,
                    self.confirmTextField.rac_textSignal]
    reduce:^(NSString *username, NSString *password, NSString *confirm){
        return @(username.length > 0 && password.length > 0 && [password isEqualToString:confirm]);
    }];
    RAC(self.registerButton, enabled) = validSignal;
}

可以看出,此时一个完整的逻辑会被聚合在一块儿,非常清晰,可读性也非常强。

至此,如果以上这些内容对你并没有什么吸引力,可以不继续往下看了。

ReactiveObjC中的基本概念与简单使用

如果让我用一句话总结RAC到底能干嘛?那就是:统一所有异步事件的回调方式。

大一统

作为iOS开发者,我们写的绝大部分代码其实都是为了响应事件发生或者状态变化:当一个按钮被点击时,需要写一个@IBAction方法来响应;当需要监听键盘是否弹出的状态时,需要注册一个Notification来响应;当使用NSURLSession做网络请求时需要提供一个block来响应;当想要监听一个属性值的变化时,需要使用KVO来响应;当一个scrollView滑动时,需要写Delegate方法来响应。

为了响应这些事件发生或者状态变化,系统提供了多种方式: Delegate, KVO, Block, Notification, Target-Action。而这也就是问题的根源,写法不统一最终一定会导致代码异常复杂与混乱。如果用一种新的方式将上述五种方式合而为一,会不会很大程度提高代码可读性?哇咔咔,那绝对是当然的。

首先就来看一下如何将五种方式回调写法统一。

//T-A
[[self.button rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
    // 按钮被点击回调
}];
//Notification
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillShowNotification object:nil] subscribeNext:^(id x) {
    // 键盘弹出回调
}];
//Block
[[self asyncDataRequest] subscribeNext:^(id x) {
    // 请求成功回调
} error:^(NSError *error) {
    // 请求错误回调
}];
//KVO
[RACObserve(self, name) subscribeNext:^(id x) {
    // 属性值变化回调
}];
//Delegate
[[self rac_signalForSelector:@selector(scrollViewDidScroll:) fromProtocol:@protocol(UIScrollViewDelegate)] subscribeNext:^(id x) {
    // scrollView滑动回调
}];

不光如此,对于任意方法也可以应用同样的回调方式。

// Method
[[self rac_signalForSelector:@selector(viewWillAppear:)] subscribeNext:^(id x) {
    // viewWillAppear方法被调用
}];

每当相应的事件触发或者状态改变时,block中的代码都会执行。没有Target-Action,没有Delegate,没有KVO,没有Notification,只有block。厉害了有木有?

观察上面代码可以发现:代码高度聚合,无需跨方法调用和传值。这样优点有:

  1. 能够减少方法数量
  2. 减少很多表示状态的中间变量
  3. 拥有足够的上下文环境,减少对其他对象的引用

理论上来说,通过上面这种方式一个类中只要有一个方法就够了,虽然现实中没有人会这样。

基本概念

RACEvent

上面提到,响应式编程可以将变化的值通过数据流进行传播。为此,RAC中定义了一个事件的概念,即:RACEvent

事件分三种类型:Next类型,Completed类型和Error类型。其中Next类型和Error类型事件内部可以承载数据,而Completed类型并不。

RACSignal

这是RAC中最基本的一个概念,中文名叫做“信号”,搞懂了这个类,就可以用RAC去玩耍了。

信号代表的是一个随时间而改变的值流。作为一个流,可以将不断变化的值(或者说数据)向外进行传播。想获取一个信号中的数据,需要订阅它。什么是订阅呢?和订阅博客,订报纸,订牛奶一个意思。但前提是这个信号是存在的,所以想要订阅必先创建。反过来说,创建了一个信号但是并没有订阅它,也获取不到其内部的数据。(这种情况下RACSignal信号根本就不会向外发送数据,下一篇中会详细介绍,暂时忽略)。当一个订阅过程结束时,如有必要去做一些清理工作(当然为了回收资源需要将信号销毁,但RAC内部会自动处理,使用者无需关心)。综上,一个信号完整的使用过程应该是创建,订阅,清理。

信号被订阅了之后,可以认为在信号源和订阅者之间建立起了一座桥梁,通过它信号源源不断的向订阅者发送最新数据,直到桥被销毁。但是要注意,这是一条很窄而且承重很差的桥,以至于一次只能通过一条数据。如果将一条数据理解成一个人,那么通俗的说就是只有一个人通过了另一个人才能继续过,而绝不能同时两个人走上桥。

信号向外传播数据的载体就是事件。其中Next类型事件可以承载任意类型数据-即id,甚至可以是nil。但一般不用来承载错误类型数据,因为有Error类型事件单独做这件事。Completed类型事件仅作为一个正常完成的标志,不承载任何数据。

信号被订阅了之后,可以发送任意多个Next事件(当然可以是0),直到发送了一个Completed事件或者一个Error事件,这两个事件都标志着结束,区别在于Completed事件表示正常结束,而Error事件表示因为某种错误而结束。只要两者之一被发送了,整个订阅过程就结束了。

根据是否会发送一个表示结束的事件信号其实可以分两类。举2个例子,网络请求可作为一个信号,去订阅它,调用API得到结果后,若成功则将获得的数据通过Next事件发出,然后跟一个Completed事件;反之则将错误原因通过一个Error事件发出。此时这个API都调用完成,整个订阅过程结束。按钮可作为另一个信号,去订阅它,之后每当按钮被点击时都会发出一个Next事件,但是在它的整个生命周期中,都不会发送Completed事件或Error事件。

看一下下面的简单示意图:

–1–2–3–4–5–6–|—-> // “|” = 正常结束:发送一个Completed事件

–a–b–c–d–e–f–X—-> // “X” = 异常结束:发送一个Error事件

–tap–tap———-tap–> // “|” = 一直发送Next事件

三种类型事件关系如下图:

RACSubscriber

信号可以被订阅,订阅了信号的对象就是(外部)订阅者。注意,信号RACSignal本身是没有发送事件能力的。为了实现向外发送事件,内部还需要一个发送者。为此,RAC首先定义了一个RACSubscriber协议,此协议中定义了发送不同事件类型的方法。然后又定义了一个实现了此协议并与其同名的类RACSubscriber。这个类中文可翻译成(内部)订阅者。当一个对象订阅了某个信号后,就会产生一次订阅行为,对应的英文是subscription。此时,RAC内部会创建一个RACSubscriber类的实例充当内部订阅者,负责发送事件。综上,虽然有两个订阅者,但是两者并不一样,不要懵逼。官方文档或者其他博客中提到的订阅者均是此处所说的内部订阅者,而本篇文章中提到的订阅者均是此处所说的外部订阅者。

举个🌰:

当你订阅了每天一次的牛奶时,这个订阅行为叫做:subscription;此时你是一个(外部)订阅者;而每天负责向你送牛奶的送奶工,就是一个(内部)订阅者:subscriber。

RACDisposable

前面提到,当一个信号的订阅过程结束时,如有必要去做一些清理工作,为此,RAC定义了一个RACDisposable类,中文可以叫做“清理者”。

综上,RAC最核心最基本的概念就是这些,四个名词类:RACEvent(事件),RACSignal(信号),RACSubscriber(发送者),RACDisposable(清理者);三个动词:创建(create),订阅(subscribe),清理(dispose)。

简单使用

创建

创建一个信号非常简单,RACSignal定义了一个类方法如下:

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe;

调用这个方法就会成功创建一个signal对象,并且其内部会自动创建一个实现RACSubscriber协议的对象subscriber,负责对外发送事件。

不难猜出,RACSubscriber协议中定义的发送三种不同事件类型的方法分别如下:

- (void)sendNext:(id)value;
- (void)sendError:(NSError *)error;
- (void)sendCompleted;

举个🌰:

RACSignal *sourceSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"👦🏻"];
    [subscriber sendCompleted];
    return [RACDisposable disposableWithBlock:^{
    }];
}];

订阅

信号有了,那如何订阅呢?仍然非常简单,RACSignal中定义了一些方法如下:

仅订阅next类型事件:

- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock;

仅订阅next和error类型事件:

- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock;

同时订阅三种类型事件:

- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock;

以及:

- (RACDisposable *)subscribeError:(void (^)(NSError *error))errorBlock;
- (RACDisposable *)subscribeCompleted:(void (^)(void))completedBlock;
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock completed:(void (^)(void))completedBlock;
- (RACDisposable *)subscribeError:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock;

根据实际想订阅内容的不同可以有选择性的使用不同的方法,通常来说上面三个比较常用。

举个🌰:

[sourceSignal subscribeNext:^(id x) {
    NSLog(@"接收到next类型事件:%@",x);
} error:^(NSError *error) {
    NSLog(@"接收到error类型事件:%@",error);
} completed:^{
    NSLog(@"接收到completed类型事件,不包含任何数据");
}];

此时,sourceSignal发送任何数据都能被接收到。

清理

注意上面createSignal方法中的block返回的是一个RACDisposable类型对象,subscribe方法返回的也是一个RACDisposable类型对象。下面就来说说这个类怎么用。

RACDisposable类有一个类方法:

+ (instancetype)disposableWithBlock:(void (^)(void))block;

就像上面展示过的,使用它就创建了一个disposable对象。block中可以写一些资源回收和垃圾清理的代码。比如如果是一个网络请求就取消这个请求,如果是打开一个文件就关闭这个文件等。

当信号的订阅过程结束时,block中的代码会自动执行。

当然,有时不需要任何清理,那么block中就是空的。这种情况也可以不返回一个RACDisposable类型对象而是直接返回一个nil

举个🌰:

RACSignal *sourceSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"👦🏻"];
    [subscriber sendCompleted];
    return nil;
}];

那信号的订阅过程如何结束呢?这里分两种情况,分别是订阅正常完成而结束和订阅中途被取消而结束。

订阅正常完成是指当信号源发送完所有的next事件后,发送一个completed或error事件。订阅中途被取消是指信号源还没有发送结束事件时订阅者就不再继续订阅了。

RAC中并没有明确定义一个取消订阅的方法,但是RACDisposable类中定义了如下一个方法:

- (void)dispose;

调用这个方法就表示不再订阅(也就是取消订阅)可以直接去清理资源了。

举个🌰:

RACDisposable *disposable = [sourceSignal subscribeNext:^(id x) {
    NSLog(@"接收到next类型事件:%@",x);
}];
[disposable dispose];

通常来说应用这种方式的情况非常少。

综上,订阅过程如下图:

下面来看一个完整的流程:

发送next类型事件以completed结束时:

// 1 信号未被创建
RACSignal *sourceSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    // 3 信号被激活,开始发送事件
    [subscriber sendNext:@"👦🏻"];
    [subscriber sendCompleted];
    return [RACDisposable disposableWithBlock:^{
        // 6 订阅流程结束,可清理资源
    }];
}];
// 2 信号已被创建,未被订阅(未激活)
[sourceSignal subscribeNext:^(id x) {
    // 4 信号已被订阅,可接收next类型事件
    NSLog(@"接收到next类型事件:%@",x);
} error:^(NSError *error) {
    // 发送next与completed类型事件时,此处不会走到
    NSLog(@"接收到error类型事件:%@",error);
} completed:^{
    // 5 信号已被订阅,可接收completed类型事件
    NSLog(@"接收到completed类型事件");
}];

结果是:

接收到next类型事件:👦🏻
接收到completed类型事件   

未发送next类型事件以error结束时:

// 1 信号未被创建
RACSignal *sourceSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    // 3 信号被激活,开始发送事件
    [subscriber sendError:[NSError errorWithDomain:@"www.reactivecocoademo.com" code:202 userInfo:nil]];
    return [RACDisposable disposableWithBlock:^{
        // 5 订阅流程结束,可清理资源
    }];
}];
// 2 信号已被创建,未被订阅(未激活)
[sourceSignal subscribeNext:^(id x) {
    // 仅发送error类型事件时,此处不会走到
    NSLog(@"接收到next类型事件:%@",x);
} error:^(NSError *error) {
    // 4 信号已被订阅,可接收error类型事件
    NSLog(@"接收到error类型事件:%@",error);
} completed:^{
    // 发送error类型事件时,此处不会走到
    NSLog(@"接收到completed类型事件");
}];

结果是:

接收到error类型事件:Error Domain=www.reactivecocoademo.com Code=202 "(null)"

其中注释前面的数字表示代码执行的先后顺序。

除此之外,在信号发送的数据被订阅者接收到之前还可以拦截到而添加一些附加操作,有点面向切片编程的意思。

接收next类型事件以及completed事件之前做些事情:

// 1 信号未被创建
RACSignal *sourceSignal = [[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    // 3 信号被激活,开始发送事件
    [subscriber sendNext:@"👧🏼"];
    [subscriber sendCompleted];
    return [RACDisposable disposableWithBlock:^{
        // 8 订阅流程结束,可清理资源
    }];
}] doNext:^(id x) {
    // 4 信号被激活,next类型事件已发送,before接收到
    NSLog(@"before 接收到next类型事件:%@",x);
}] doError:^(NSError *error) {
    // 发送next与completed类型事件时,此处不会走到
}] doCompleted:^{
    // 6 信号被激活,before发送completed类型事件
}];
// 2 信号已被创建,未被订阅(未激活)
[sourceSignal subscribeNext:^(id x) {
    // 5 信号已被订阅,可接收next类型事件
    NSLog(@"接收到next类型事件:%@",x);
} error:^(NSError *error) {
    // 发送next与completed类型事件时,此处不会走到
} completed:^{
    // 7 信号已被订阅,可接收completed类型事件
    NSLog(@"接收到completed类型事件");
}];

结果是:

before 接收到next类型事件:👧🏼
接收到next类型事件:👧🏼
接收到completed类型事件

接收error类型事件之前做些事情:

// 1 信号未被创建
RACSignal *sourceSignal = [[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    // 3 信号被激活,开始发送事件
    [subscriber sendError:[NSError errorWithDomain:@"www.reactivecocoademo.com" code:202 userInfo:nil]];
    return [RACDisposable disposableWithBlock:^{
        // 6 订阅流程结束,可清理资源
    }];
}] doNext:^(id x) {
    // 仅发送error类型事件时,此处不会走到
}] doError:^(NSError *error) {
    // 4 信号被激活,error类型事件已发送,before接收到
    NSLog(@"before 接收到error类型事件:%@",error);
}] doCompleted:^{
    // 发送error类型事件时,此处不会走到
}];
// 2 信号已被创建,未被订阅(未激活)
[sourceSignal subscribeNext:^(id x) {
    // 仅发送error类型事件时,此处不会走到
} error:^(NSError *error) {
    // 5 信号已被订阅,可接收error类型事件
    NSLog(@"接收到error类型事件:%@",error);
} completed:^{
    // 发送error类型事件时,此处不会走到
}];

结果是:

before 接收到error类型事件:Error Domain=www.reactivecocoademo.com Code=202 "(null)"
接收到error类型事件:Error Domain=www.reactivecocoademo.com Code=202 "(null)"    

See? 就是这么简单。

种类丰富而神奇的操作符

RAC定义了一系列用来组合,转换,过滤值流的操作符,熟练使用它们可以大幅度减化代码复杂度,从而提高开发效率。

举两个例子:

🌰 1: 一个页面的数据来源于三个接口,且这三个接口必须顺序请求,也就是有一个严格的依赖关系。都调用之后一次性去刷新页面。

应用RAC定义的操作符号可类似如下实现:

[[[[self asyncDataRequest1]
   then:^RACSignal *{
    return [self asyncDataRequest2];
}] flattenMap:^RACStream *(id value) {
    return [self asyncDataRequest3WithRequest2Result:value];
}] subscribeNext:^(id x) {
    [self refreshUIWithData:x];
} error:^(NSError *error) {
    [self showError:error];
}];

🌰 2: 一个label的数据仍然来源于三个接口,但这三个接口可并发请求。都调用之后一次性去刷新label

应用RAC定义的操作符号可类似如下实现:

[[RACSignal combineLatest:@[[self asyncDataRequest1], [self asyncDataRequest2], [self asyncDataRequest3]] reduce:^(id data1, id data2, id data3){
   return [NSString stringWithFormat:@"data1:%@ data2:%@ data3:%@",(NSString *)data1,(NSString *)data2,(NSString *)data3];
}] subscribeNext:^(id x) {
   [self refreshLabelWithData:x];
} error:^(NSError *error) {
   [self showError:error];
}];

有木有觉得比之前的方式稍微简单一点点?

言归正传,根据是否作用于数据本身操作符可分为两大类型,此处仅涉及是的情况,下一篇中会介绍一些其他的。

正式介绍操作符之前先介绍另一个基础的概念-元组,方便后面理解。

RACTuple

你一定为当跨方法传多个值时是选择每个值作为一个参数还是选择传一个字典而纠结过。第一种方法可能会令方法名特别长,第二种方法根本不知道字典里面都有哪些key。为了避免陷入此尴尬,RAC定义了一个RACTuple类。它可以包含多个对象。酷毙了!

为了简化使用,RAC额外又定义了两个宏:RACTuplePack(...)RACTupleUnpack(...),分别用来“装包”和“拆包”。

举个🌰:

NSString *when = @"2017-09-19";
NSString *where = @"Beijing";
NSString *who = @"ZhouXingXing";
RACTuple *tuple = RACTuplePack(when, where, who);
RACTupleUnpack(NSString *time, NSString *place, NSString *person) = tuple;
NSLog(@"Three element are : %@, %@, %@",time,place,person);

结果是:

Three element are : 2017-09-19, Beijing, ZhouXingXing

组合操作符

将多个信号组合成一个信号。

concat

按顺序拼接信号,订阅者依次接收每个信号发出的值。

举个🌰:

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🐶"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🐱"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalC = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🐭"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *concatSignal = [[signalA concat:signalB] concat:signalC];
[concatSignal subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🐶
🐱
🐭

then

连接多个信号,订阅者只接收最后一个信号发出的值。

举个🌰:

[[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🐭"];
    [subscriber sendCompleted];
    return nil;
}] then:^RACSignal *{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"🐹"];
        [subscriber sendCompleted];
        return nil;
    }];
}] then:^RACSignal *{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"🐸"];
        [subscriber sendCompleted];
        return nil;
    }];
}] subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🐸

merge

将多个信号合并成一个信号,订阅者订阅合并后的信号就相当于同时订阅了多个源信号。简单的说就是并。

举个🌰:

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🐭"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🐹"];
    [subscriber sendNext:@"🐰"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalC = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🐸"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *mergeSignal = [[signalA merge:signalB] merge:signalC];
[mergeSignal subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🐭
🐹
🐰
🐸

zipWith

将两个信号压缩成一个新信号,新信号将两个源信号发出的值组合成一个元组类型发出。

需要注意的是两个源信号都要发出一个值,压缩后的信号才会发出一个值,也就是存在一个一一对应的关系。

举个生活中的例子,做肉夹馍时,肉和馍都属于原材料,相当于源值,一份肉严格对应一个馍,夹在一起之后才算是一个成品,相当于合成一个新的值。一种材料用完之后无论另一种剩下多少都不能做成新的肉夹馍,相当于都不能合成新的事件。

举个🌰:

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🅰️"];
    [subscriber sendNext:@"🅱️"];
    [subscriber sendNext:@"🆎"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"1️⃣"];
    [subscriber sendNext:@"2️⃣"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *zipSignal = [signalA zipWith:signalB];
[zipSignal subscribeNext:^(RACTuple *tuple) {
    RACTupleUnpack(NSString *x1, NSString *x2) = tuple;
    NSLog(@"%@ %@",x1,x2);
}];

结果是:

🅰️ 1️⃣
 🅱️ 2️⃣

combineLatestWith

将两个信号组合成一个新信号,每个源信号都至少发出一个值之后新信号拿到每个源信号发出的最新值将其组合成一个元组类型发出。

举个🌰:

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🅰️"];
    [subscriber sendNext:@"🅱️"];
    [subscriber sendNext:@"🆎"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"1️⃣"];
    [subscriber sendNext:@"2️⃣"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *combineLatestSignal = [signalA combineLatestWith:signalB];
[combineLatestSignal subscribeNext:^(RACTuple *tuple) {
    RACTupleUnpack(NSString *x1, NSString *x2) = tuple;
    NSLog(@"%@ %@",x1,x2);
}];

结果是:

🆎 1️⃣
🆎 2️⃣

combineLatest

将多个信号组合成一个新信号,其余同上。

举个🌰:

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🅰️"];
    [subscriber sendNext:@"🅱️"];
    [subscriber sendNext:@"🆎"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"1️⃣"];
    [subscriber sendNext:@"2️⃣"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalC = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"①"];
    [subscriber sendNext:@"②"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *combineLatestSignal = [RACSignal combineLatest:@[signalA,signalB,signalC]];
[combineLatestSignal subscribeNext:^(RACTuple *tuple) {
    RACTupleUnpack(NSString *x1, NSString *x2, NSString *x3) = tuple;
    NSLog(@"%@ %@ %@",x1,x2,x3);
}];

结果是:

🆎 2️⃣ ①
🆎 2️⃣ ②

reduce

当信号发出的数据是元组类型时,将元组内的多个值聚合成一个新的值。

一般搭配 combineLatest 使用。

举个🌰:

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🅰️"];
    [subscriber sendNext:@"🅱️"];
    [subscriber sendNext:@"🆎"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"1️⃣"];
    [subscriber sendNext:@"2️⃣"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *signalC = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"①"];
    [subscriber sendNext:@"②"];
    [subscriber sendCompleted];
    return nil;
}];
RACSignal *reduceSignal = [RACSignal combineLatest:@[signalA,signalB,signalC] reduce:^id(NSString *x1, NSString *x2, NSString *x3){
    return [NSString stringWithFormat:@"%@ %@ %@",x1,x2,x3];
}];
[reduceSignal subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🆎 2️⃣ ①
🆎 2️⃣ ②

变换操作符

对源信号发出的值进行转换。

map

将源信号中发出的每一个next类型事件中的原值转换成一个新的值,订阅者接收到的是转换后的新值。

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"⚽️"];
    [subscriber sendNext:@"🏀"];
    [subscriber sendCompleted];
    return nil;
}] map:^id(id value) {
    if ([value isEqualToString:@"⚽️"]) {
        return @"🏈";
    }
    else if ([value isEqualToString:@"🏀"]) {
        return @"⚾️";
    }
    return nil;
}] subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🏈
⚾️

flattenMap

map是将源信号中发出的每一个next类型事件中的原值转换成一个新的值,但如果是将源值转换成一个新的信号呢?此时实际关心的值其实是新的信号所发送的事件中的值,为了避免订阅套订阅,此时应该应用flattenMap

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🏀"];
    [subscriber sendCompleted];
    return nil;
}] flattenMap:^RACStream *(id value) {
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
             [subscriber sendNext:@"🎾"];
             [subscriber sendCompleted];
             return nil;
    }];
}] subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🎾

过滤和条件操作符

过滤或筛选掉源信号发出的一些事件。

filter

过滤掉一些不满足条件的值。

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🍎"];
    [subscriber sendNext:@"🍐"];
    [subscriber sendCompleted];
    return nil;
}] filter:^BOOL(id value) {
    return ![value isEqualToString:@"🍎"];
}] subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🍐

ignore

忽略掉一些满足条件的值。

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🍎"];
    [subscriber sendNext:@"🍐"];
    [subscriber sendCompleted];
    return nil;
}] ignore:@"🍎"]
   subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🍐

distinctUntilChanged

阻止源信号连续发送相同值。

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🍌"];
    [subscriber sendNext:@"🍌"];
    [subscriber sendNext:@"🍉"];
    [subscriber sendNext:@"🍇"];
    [subscriber sendCompleted];
    return nil;
}] distinctUntilChanged]
 subscribeNext:^(id x) {
     NSLog(@"%@",x);
}];

结果是:

🍌
🍉
🍇

take

使订阅者只接收到固定个数的值,从最初一个顺序计算。

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🍎"];
    [subscriber sendNext:@"🍐"];
    [subscriber sendNext:@"🍊"];
    [subscriber sendCompleted];
    return nil;
}] take:2]
   subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🍎
🍐

takeLast

使订阅者只接收到固定个数的值,从最后一个倒序计算。

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🍎"];
    [subscriber sendNext:@"🍐"];
    [subscriber sendNext:@"🍊"];
    [subscriber sendCompleted];
    return nil;
}] takeLast:2]
 subscribeNext:^(id x) {
     NSLog(@"%@",x);
}];

结果是:

🍐
🍊

takeUntil

订阅某个信号直到另一个信号执行。

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🍎"];
    [subscriber sendNext:@"🍐"];
    [subscriber sendNext:@"🍊"];
    [subscriber sendCompleted];
    return nil;
}] takeUntil:self.rac_willDeallocSignal]
   subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

结果是:

🍎
🍐
🍊

skip

使订阅者跳过固定个数的值再开始接收。

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🍎"];
    [subscriber sendNext:@"🍈"];
    [subscriber sendNext:@"🍌"];
    [subscriber sendNext:@"🍉"];
    [subscriber sendCompleted];
    return nil;
}] skip:1]
 subscribeNext:^(id x) {
     NSLog(@"%@",x);
}];

结果是:

🍈
🍌
🍉

throttle

节流:为了避免信号发送事件比较频繁时,可以使用节流。在某一段时间不发送事件,过了一段时间获取信号的最新内容发出。

举个🌰:

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"🍊"];
    [subscriber sendNext:@"🍑"];
    sleep(1);
    [subscriber sendNext:@"🍉"];
    [subscriber sendCompleted];
    return nil;
}] throttle:1]
 subscribeNext:^(id x) {
     NSLog(@"%@",x);
}];

结果是:

🍉

当然,这里只是一部分。实际开发过程中,需求多种多样,经常会出现一个功能因为对信号的不同组合和链式操作使得有多种实现方式的情况,尤其是相对复杂的需求。因此RAC非常适合发挥想象力。

最后,再来看一眼文章开头出现过的图片,有没有感觉豁然开朗?

参考链接

官方文档:

ReactiveObjCIntroduction
ReactiveObjCDocumentation

宏观介绍:

Reactive​Cocoa

入门经典:

ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2
ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2

罕见的国人写的高质量文章:

iOS开发下的函数响应式编程
ReactiveCocoa v2.5 源码解析之架构总览

我之前写的:

ReactiveCocoaDemo
RxSwift基本概念与使用