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可以传递value,error,complete三种值,在一个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
RACSequence
和RACSignal
可以相互转换,把RACSequence
转换成RACSignal
需要把sequence里面的值放到一个RACScheduler
中,RACScheduler
之后会把值顺序的推(push)到新创建的RACSignal
中,就像下面这样:
1 | RACSequence *sequence = [@[@1, @2, @3] rac_sequence]; |
RACSignal
也可以通过几种中间类型转化为RACSequence
,首先收集所有发送到这个signal的值,直到signal收到complete值,之后像下面这样转化:
1 | RACSignal *signal = ...; |
我们需要注意的是-toArray
方法是阻塞的,他会阻塞当前线程直到signal收到complete值,但是这里我们不知道将来还会有多少值,所以这里可能会有问题。
另外一种不等待complete值的方法把RACSignal
成RACSequence
的方法就是-sequence
方法,这个方法创建一个内部使用RACReplaySubject
的RACSignalSequence
,这个内部对象拥有所有的传递到signal的值,并且当访问的时候,才会吧signal到现在为止收到的所有值转化成sequence,就像下面这样:
1 | RACSignal *signal = ...; |
虽然说-sequence
方法不会等待signal收到complete值,但是如果signal还没有收到任何值,那么依然会阻塞当前线程直到收到第一个值。
一般来说,我们更多关注RACSignal
因为他代表了将要到来的值,而RACSequence
大多数情况下只是为了方便的在集合对象上使用sequence的操作。
Signal 的来源
一般程序中的大部分signal是衍生(derived)得到的signal,也就是说signal大部分是通过对别的signal进行操作转化而来的,比如下面这个:
1 | RACSignal *existingSignal; |
上面的例子里面newSingal
发送someAction(value)
的结果作为值,并把nil
的值过滤掉,这里实际上创建了两个signal,第一个是通过-map:
方法创建的,第二个是通过对第一个创建的signal调用-ignore:
方法创建的,这些方法都会创建新的signal。
但是呢,最初的signal是从哪里来的呢?如何像一个信号里面发送值呢?我们来看下面的内容
KVO 监听
在ViewModel(还未翻译)那篇里面看到过的一个例子是通过KVO的监听器来创建signal:
1 | RACSignal *signal = RACObserve(object, propertyToObserve); |
当object
的propertyToObserve
发生变化的时候signal会收到一个值,RACObserve
是一个展开为-rac_valuesForKeyPath:observer:
的宏。这里有一个要注意的地方是:这个红,总是使用self
作为监听者,所以,如果在block里面使用这个宏的话,要注意循环引用的问题。
UIKit 的 Category 们
ReactiveCocoa提供了一组UIKit的Category为用户事件来产生signal。说起UIKit 的强大,正如大家所知,KVO功不可没。(感谢热心评论的小伙伴)
RACSubject
RACSubject
是连接rac代码和非rac代码的桥梁,RACSubject
是一个RACSignal
(继承而来)但是可以手动push值到里面,就像下面这样:
1 | RACSubject *subject = [RACSubject subject]; |
动态 signal 们
动态signal的背后是一个叫RACDynamicSignal
的私有类,因为这样的signal是开发者创建的,所以叫“动态”的signal。我们可以吧任何的异步动作转换为一个signal,就像下面这样:
1 | RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { |
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 | RACSignal *mapSignal = [signal map:^id (id value) { |
另一个需要注意的是订阅的block会为每个订阅者执行一次,所以如果我们再创建另一个源自上面signal的signal:
1 | RACSignal *anotherMapSignal = [signal map:^id (id value) { |
这样会对原来的signal创建另一个订阅并且会再一次调用订阅的block,但是在一些特定的情况下,我们不希望异步的行为被多次的执行,RACConnection
是解决这个问题的方法,简单点说,RACConnection
可以在订阅者之间通过他内部的RACSubject
来分享没有送出去的signal,后面会详细说吧。。。
严格的说,上面的例子,没有一个发生实际的订阅,因为-map:
内部只是创建了一个动态signal,并且订阅原来的signal只有在通过订阅被-map:
创建的signal的时候才会发生。这也意味着订阅是按照订阅链顺序反向发生的,为了触发这个订阅链,我们必须真正的去直接订阅一个signal,就像下面这两种形式:
1 | [anotherMapSignal subscribeNext:^(id value) { |
或者
1 | RAC(object, property) = anotherMapSignal; |
最后欢迎大家订阅我的微信公众号 Little Code
- 公众号主要发一些开发相关的技术文章
- 谈谈自己对技术的理解,经验
- 也许会谈谈人生的感悟
- 本人不是很高产,但是力求保证质量和原创