知识共享许可协议本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。本文仅作为个人学习记录使用,欢迎在许可协议范围内转载或使用,请尊重版权并且保留原文链接,谢谢您的理解合作。如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,这样您将能在第一时间获取本站信息。

start

在小伙伴们都玩了好久RAC之后,我才开始研究如何使用-,-思来想去发现这东西看上去似乎很复杂,用起来确实也很复杂,里面涉及到很多概念是常规iOS或是OS X开发过程中完全没用到过的,同时RAC也将函数式编程的思想带到了iOS开发中。

在查找资料的过程中,找到了一个网站ReactiveCocoa Design Patterns这个网站里面对RAC的一些基础观念进行了比较好的解释,所以为了让自己能更好的学习,就决定翻译翻译,也希望能对大家学习过程中有帮助。

本文翻译自ReactiveCocoa Design Patterns

注意:下文中,为了方便大家理解,部分单词没有翻译,当然如果需要知道中文的,如下:

  • signal:信号
  • sequence:顺序序列
  • stream:流
  • pull-driven:拉驱动
  • push-driven:推驱动
  • value:值
  • error:错误
  • complete:完成

Signals

Signal是构造FRP程序最基础的砖块,一般来说,我们的程序组织一系列的signal,决定这些signal的值从何而来,signal之间如何连接,以及值如何在signal中传递。我们可以认为signal是一个管道,我们的应用程序是一个复杂的管道系统,将事件放入管道系统的输入端,从输出端得到结果。

ReactiveCocoa 中的 signals

ReactiveCocoa 2.x的框架中使用RACStream对signal进行了抽象和包装,一个stream可以传递valueerrorcomplete三种值,在一个RACStream接收了error或者complete之后,那么就不会在传递任何发送给他的值了。

Stream有两种类型,一种是push-driver的RACSignal,一种是pull-driven的RACSequence

  • push-driven:表示这个stream在创建的时候是没有值的,只有在将来(接收到网络请求,或是用户输入事件)的时候才会有值。
  • pull-driven:表示这个stream在创建的时候就已经有值了,并且我们可以从stream中顺序的获取里面的值,在ReactiveCocoa中通常通过Cocoa的集合对象(比如NSArray,NSSet,NSDictionary或者NSIndexSet)创建。

未来的ReactiveCocoa 3中将会废弃sequences,不过这是后话了。

RACSequence <=> RACSignal

RACSequenceRACSignal可以相互转换,把RACSequence转换成RACSignal需要把sequence里面的值放到一个RACScheduler中,RACScheduler之后会把值顺序的推(push)到新创建的RACSignal中,就像下面这样:

1
2
RACSequence *sequence = [@[@1, @2, @3] rac_sequence];
RACSignal *signal = [sequence signalWithScheduler:[RACScheduler mainThreadScheduler]];

RACSignal也可以通过几种中间类型转化为RACSequence,首先收集所有发送到这个signal的值,直到signal收到complete值,之后像下面这样转化:

1
2
3
RACSignal *signal = ...;
NSArray *buffer = [signal toArray];
RACSequence *sequence = [buffer rac_sequence];

我们需要注意的是-toArray方法是阻塞的,他会阻塞当前线程直到signal收到complete值,但是这里我们不知道将来还会有多少值,所以这里可能会有问题。

另外一种不等待complete值的方法把RACSignalRACSequence的方法就是-sequence方法,这个方法创建一个内部使用RACReplaySubjectRACSignalSequence,这个内部对象拥有所有的传递到signal的值,并且当访问的时候,才会吧signal到现在为止收到的所有值转化成sequence,就像下面这样:

1
2
RACSignal *signal = ...;
RACSequence *sequence = [signal sequence];

虽然说-sequence方法不会等待signal收到complete值,但是如果signal还没有收到任何值,那么依然会阻塞当前线程直到收到第一个值。

一般来说,我们更多关注RACSignal因为他代表了将要到来的值,而RACSequence大多数情况下只是为了方便的在集合对象上使用sequence的操作。

Signal 的来源

一般程序中的大部分signal是衍生(derived)得到的signal,也就是说signal大部分是通过对别的signal进行操作转化而来的,比如下面这个:

1
2
3
4
RACSignal *existingSignal;
RACSignal *newSignal = [[existingSignal map:^ id (id value) {
return someAction(value);
}] ignore: nil];

上面的例子里面newSingal发送someAction(value)的结果作为值,并把nil的值过滤掉,这里实际上创建了两个signal,第一个是通过-map:方法创建的,第二个是通过对第一个创建的signal调用-ignore:方法创建的,这些方法都会创建新的signal。

但是呢,最初的signal是从哪里来的呢?如何像一个信号里面发送值呢?我们来看下面的内容

KVO 监听

ViewModel(还未翻译)那篇里面看到过的一个例子是通过KVO的监听器来创建signal:

1
RACSignal *signal = RACObserve(object, propertyToObserve);

objectpropertyToObserve发生变化的时候signal会收到一个值,RACObserve是一个展开为-rac_valuesForKeyPath:observer:的宏。这里有一个要注意的地方是:这个红,总是使用self作为监听者,所以,如果在block里面使用这个宏的话,要注意循环引用的问题。

UIKit 的 Category 们

ReactiveCocoa提供了一组UIKit的Category为用户事件来产生signal。说起UIKit 的强大,正如大家所知,KVO功不可没。(感谢热心评论的小伙伴)

RACSubject

RACSubject是连接rac代码和非rac代码的桥梁,RACSubject是一个RACSignal(继承而来)但是可以手动push值到里面,就像下面这样:

1
2
3
4
5
RACSubject *subject = [RACSubject subject];
RACSignal *derived = [subject map:^(id value) {
return someAction(value);
}];
[subject sendNext:@YES]; // 这里就把YES的值push给subject,然后push给derived这个signal了

动态 signal 们

动态signal的背后是一个叫RACDynamicSignal的私有类,因为这样的signal是开发者创建的,所以叫“动态”的signal。我们可以吧任何的异步动作转换为一个signal,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
id operation = asynchronusAction(^(id asyncActionResult, NSError *error) {
if (error) {
[subscriber sendError:error];
}
else {
[subscriber sendNext:asyncActionResult];
[subscriber sendCompleted];
}
});
return [RACDisposable disposableWithBlock:^{
[operation cancel];
}];
}];

asynchronusAction()需要一个block参数。

这里是这样的:当第二个signal订阅第一个signal的时候将会调用我们传入给-createSignal:的block,并且把subscriber作为一个值传递给了下一个signal(RACSignal也实现了RACSubscriber接口),要注意的是,这个时候asynchronusAction()并没有被真正的调用,只有当有对象订阅这个动态signal的时候才会调用,在block定义的行为,只有在被订阅的时候发生,我们也可以这么认为,动态signal只有在有对象订阅它的时候才会打开。
(这一段在原文里面是放到了Subscription(订阅)那一节,但是看了一下觉得实际上应该是放在这里的内容)

不用怕上面的”disposable”的内容,这里简单说下:在异步的操作能被取消的情况,我们需要在signal被销毁/释放(在RAC里面就是disposed)时候取消这个操作,将来也将会详细的说。

订阅

在上面的一个例子里面我们见过“订阅”了,这是RAC里面signal的基础设置,signal通过订阅相互连接起来,下面让我们假装需要通过map来从一个signal创建另一个新的signal:

1
2
3
RACSignal *mapSignal = [signal map:^id (id value) {
return someAction(value);
}];

另一个需要注意的是订阅的block会为每个订阅者执行一次,所以如果我们再创建另一个源自上面signal的signal:

1
2
3
RACSignal *anotherMapSignal = [signal map:^id (id value) {
return anotherAction(value);
}];

这样会对原来的signal创建另一个订阅并且会再一次调用订阅的block,但是在一些特定的情况下,我们不希望异步的行为被多次的执行,RACConnection是解决这个问题的方法,简单点说,RACConnection可以在订阅者之间通过他内部的RACSubject来分享没有送出去的signal,后面会详细说吧。。。

严格的说,上面的例子,没有一个发生实际的订阅,因为-map:内部只是创建了一个动态signal,并且订阅原来的signal只有在通过订阅被-map:创建的signal的时候才会发生。这也意味着订阅是按照订阅链顺序反向发生的,为了触发这个订阅链,我们必须真正的去直接订阅一个signal,就像下面这两种形式:

1
2
3
[anotherMapSignal subscribeNext:^(id value) {
// 代码
}];

或者

1
RAC(object, property) = anotherMapSignal;

最后欢迎大家订阅我的微信公众号 Little Code

公众号

  • 公众号主要发一些开发相关的技术文章
  • 谈谈自己对技术的理解,经验
  • 也许会谈谈人生的感悟
  • 本人不是很高产,但是力求保证质量和原创