剑客
关注科技互联网

ReactiveCocoa核心元素与信号流

概述

ReactiveCocoa(以下简称“RAC”)是一个函数响应式编程框架,它能让我们脱离Cocoa API的束缚,给我们提供另外一套编码的思路与可能性,它能在宏观层面上提升代码易读性与稳定性,让程序员写出富有“诗意”的代码,因此备受业内推崇。本文略过RAC基本概念与基础使用(有些技术点可以参考美团点评技术博客之前的几篇文章: RACSignal,冷信号与热信号系列,内存泄漏 。),着重介绍RAC数据流方面的内容,剖析RAC核心元素与RAC Operation在数据流中扮演的角色,并从数据流的角度切入,介绍RACComand与RACChannel。

RAC核心元素与管线

在绘制UI时,我们常希望能够直接获取所需数据,但大多数情况下,数据需要经过多个步骤处理后才可使用,好比UI使用到的数据是经过流水线加工后最后一端产出的成品。众所周知,流水线是由多个片段管线组成,上端管线处理后的已加工品成为下端管线的待加工品,每段管线都有对应的管线工人来完成加工工作,直至成品完成。RAC则为我们提供了构建数据流水线的能力,通过组合不同的加工管线来导出我们想要的数据。想要构建好RAC的数据流水线,我们需要先了解流水线中的组成元素——RAC管线。RAC管线的运作实质上就是RAC中一个信号被订阅的完整过程。下面我们来分析下RAC中一个完整的订阅过程,并由此来了解RAC中的核心元素。

RAC核心是Signal,对应的类为RACSignal。它其实是一个信号源,Signal会给它的订阅者(Subscriber)发送一连串的事件,一个Signal可比作流水线中的一段管线,负责决定管线传输什么样的数据。Subscriber是Signal的订阅者,我们将Subscriber比作管线上的工人,它在拿到数据后对其进行加工处理。数据经过加工后要么进入下一条管线继续处理,要么直接被当做成品使用。通过RAC管线这个比方,我们来详细了解下RAC中Signal的完整订阅过程:

  • 管线的设计-createSingal:
RACSignal *pipeline = [RACSignal createSignal:^RACDisposable*(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@(1)];
    [subscriber sendNext:@(2)];
    [subscriber sendNext:@(3)];

    [subscriber sendCompleted]; // (1)

    return[RACDisposable disposableWithBlock:^{ // (2)
        NSLog(@"the pipeline has sent 3 values, and has been stopped");
    }];
}];

这里RAC通过类簇的方式,使用RACSignal 的createSignal 方法创建了一个RACDynamicSignal对象(RACSignal的子类), 同时传入对应的didSubscribeBlock参数。这个block里,我们定义了该Signal将按何种方式、发送何种信号值。如文中的pipeline signal在顺序发出了 1、 2、 3 三个数据后,发出结束信号 (1),并且安排好信号终止订阅时的收尾工作 (2),这个过程好比是我们预先设计好一段管线,设定好管线启动后按照何种逻辑,传送出何种数据。但管线传送出待加工数据后需要有工人对其进行加工处理,于是RAC引入了Subscriber。

  • 管线工人 - Subscriber:
RACSubscriber *worker = [RACSubscriber subscriberWithNext:nextBlock error:errorBlock completed:completedBlock];

Subscriber我们一般称之为订阅者,它负责处理Signal传出的数据。Subscriber初始化的时候会传入nextBlock、 errorBlock、completeBlock,正是这三个block用于处理不同类型的数据信号,处理后的数据或者被抛往下一段管线,亦或者被当做成品送给使用方。Subscriber好比是管线上的工人,负责加工管线上传递过来的各种信号值,不过一旦Subscriber接收到error信号或complete信号,Subscriber会通过相关的RACDisposal主动取消这次订阅,停止管线的运作。那么管线有了,管线上的装配工人有了,如何启动管线?

  • 启动管线 - subscribe:
RACDisposable *disposable = [pipeline subscribe:worker]

通过RACDynamicSignal中的subscribe方法,pipeline signal(RACSignal)开始被worker(RACSubscriber)订阅。在subscribe方法中, pipeline会在执行createSignal时传入didSubscribeBlock,执行的过程遵循之前关于管线的设定,worker将接受到3个数据值与一个complete信号,并使用subscriber中的nextBlock与completeBlock对信号值分别进行处理。管线启动后,会返回一个RACDisposable对象。外部可以通过[RACDisposable dispose]方法随时停止这段管线的工作。一旦管线停止,subscriber worker将不再处理管线传送过来的任何类型的数据。详细的剖析可以参看 http://tech.meituan.com/RACSignalSubscription.html。

以上三个步骤构成了一个普通信号的订阅流程。但往往在使用RAC时,我们看不到后两者,这是因为RAC将Subscriber的初始化以及[signal subscribe: subscriber]统一封装到[signal subscribeNext: error: completed:]方法中了,如下图所示。这种封装成功屏蔽掉了Subscriber这个概念,进一步简化信号的订阅逻辑,使其更加便于使用。(PS:流水线worker永远都在默默付出!!)

ReactiveCocoa核心元素与信号流

可以发现,按照上面的订阅流程,信号只有被订阅时才会送出信号值,这种信号我们称之为冷信号(cold signal)。既然有冷信号的概念,就肯定有与之对应的热信号(hot signal)。冷信号好比只有给管线分配工人后,管线才会开启。而热信号就是在管线创建之后,不管是否有配套的工人,管线都会开始运作,可以随时根据外部条件送出数据。送出数据时,如果管线上有工人,数据被工人加工处理,如果没有工人,数据将被抛弃。以下我们仍然从信号的订阅步骤对比冷热信号:(热信号对应的类RACSubject)

  • 创建信号。与冷信号不同,RACSubject在被创建后将维护一个订阅者数组(subscribers),用于随时存储加入进来的Subscriber。此外不同于RACDynamicSignal,RACSubject在创建时并不去设定要输出的数据,而是通过实现

    协议,允许外部直接使用[RACSubject sendNext:]随时输出数据。

  • 创建订阅者。该创建过程与冷信号完全相同,即提前准备好Subscriber对应的nextBlock、errorBlock、completedBlock。

RACSubscriber *worker = [RACSubscriber subscriberWithNext:nextBlock error:errorBlock completed:completedBlock];
  • 订阅。RACSubject(hotSignal)与RACDynamicSignal(coldSignal)内部都有继承于父类RACSignal的subscribe方法,但实现过程却完全不同。RACDynamicSignal的subscribe会去执行createSignal时准备好的didSubscribeBlock,同时将subscriber传给didSubscribeBlock,让subscriber按照设定好的方式处理相应的数据值。 而热信号RACSubject仅仅只是将subscriber加入到订阅者数组中,其它啥事不干。

  • 热信号没有提前规划订阅时信号的输出,因而它需要由外部来控制信号何时输出何种数据值,于是RACSubject实现了

    协议,向外提供了[RACSubject sendNext:]、[RACSubject sendError:]、[RACSubject sendComplete:]方法。以sendNext举例,每当使用 [RACSubject sendNext] 时,RACSubject就会遍历一遍自己的subscribers数组,并调用各数组元素(subscriber)准备好的sendNextBlock (1)。

- (void)sendNext:(id)value 
{
    [self enumerateSubscribersUsingBlock:^(id<RACSubscriber> subscriber) {
        [subscriber sendNext:value]; // (1)
    }];
}

以上是冷、热信号在执行层面上的异同。有时为了消灭副作用或着其它某种原因,我们需要将冷信号转成热信号,让它具备热信号的特性。 这时候我们可以用到[RACDynamicSignal multicast: RACSubject] ,这个方法究其原理也是利用到了RACSubject可随时sendNext的这一特性。具体冷热信号的转换可参见: http://tech.meituan.com/talk-about-reactivecocoas-cold-signal-and-hot-signal-part-3.html 。此外,RACSubject有个子类RACReplaySubject。相较于RACSubject,RACReplaySubject能够将之前信号发出的数据使用valuesReceived数组保存起来, 当信号被下一个Subscriber订阅时,它能够将之前保存的数据值按顺序传送给新的Subscriber。

这一节大概介绍了RACDynamicSignal、 RACSubject、 RACSubscriber、 RACDisposal在订阅过程中扮演的角色, 事实上调度器RACSchedule也是RAC中非常重要的基础元素。RAC对它的定义是"schedule: control when and where the work the product",它对GCD做了一层很薄的包装。它能够:1.让RACSignal送出的信号值在线程中华丽地穿梭;2.延迟或周期执行block中的内容; 3.可以添加同步、异步任务,同时能够灵活取消异步添加的未执行任务。RACSchedule的使用会在下文提到。

RAC信号流

RAC流水线由多段管线组合而成,上节介绍了单条RAC管线的运作,这一节主要介绍:1.RAC管线间的衔接 — RAC Operation;2.RAC信号流的构建。

RAC Operation 作为信号值的中转站,它会返回一个新信号N。如下图所示,RAC Operation对原信号O传出的值进行加工,并将处理好的数值作为新信号N的输出,这个过程好比将原管线数据加工后抛往一段新的管线,一条RAC流水线就是由各种各样的RAC Operation组合而成的。RAC 提供了许多RACSignal Operation方便我们使用 ,其中[RACSignal bind:]操作是信号变换的核心。因此在剖析RAC Operation的时候,我们主要针对bind以及其衍生出来的flattenMap、 map、flatten进行介绍。随后将RAC流水线应用于一个具体业务需求,详细了解整段RAC信号流的构建。

ReactiveCocoa核心元素与信号流

首先我们来解读bind极其衍生出来的几个Operation:

(1) bind函数会返回一个新的信号N。整体思路是对原信号O进行订阅,每当信号O产生一个值就将其转变成一个中间信号M,并马上订阅M, 之后将信号M的输出作为新信号N的输出。管线图如下:

ReactiveCocoa核心元素与信号流

具体来看源码(为方便理解,去掉了源代码中RACDisposable, @synchronized, @autoreleasepool相关代码)。当新信号N被外部订阅时,会进入信号N 的didSubscribeBlock( 1处),之后订阅原信号O (2),当原信号O有值输出后就用bind函数传入的bindBlock将其变换成中间信号M (3), 并马上对其进行订阅(4),最后将中间信号M的输出作为新信号N的输出 (5), 如上图所示。可以说ReactiveCocoa是根据 Monad 的概念搭建起来的,而bind函数是monad的重要实现函数。

- (RACSignal *)bind:(RACStreamBindBlock (^)(void))block {
    return [RACSignal createSignal:^(id<RACSubscriber> subscriber) { // (1)
        RACStreamBindBlock bindingBlock = block();

        [self subscribeNext:^(id x) { // (2)
            BOOL stop = NO;
            id middleSignal = bindingBlock(x, &stop); // (3)

            if (middleSignal != nil) {
                RACDisposable *disposable = [middleSignal subscribeNext:^(id x) { // (4)
                    [subscriber sendNext:x]; // (5)
                } error:^(NSError *error) {
                    [subscriber sendError:error];
                } completed:^{
                    [subscriber sendCompleted];
                }];
            }
        } error:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            [subscriber sendCompleted];
        }];

        return nil
    }];
}

看完代码,我们再回到bind的管线图。每当original signal送出一个红球信号后,bind方法内部就会生成一个对应的middle signal。第一个middle signal送出的是绿球,第二个middle signal送出的是紫球,第三个middle signal送出是蓝球。由于在bind操作中,中间信号的输出将直接作为新信号的输出,因此我们可以看到图中的new signal输出的就是绿球、紫球、蓝球等,它相当于是所有middle signal输出值的集合。

(2) flattenMap:在RAC的使用中,flattenMap这个操作较为常见。事实上flattenMap是对bind的包装,为bind提供bindBlock。因此flattenMap与bind操作实质上是一样的(管线图可直接参考bind),都是将原信号传出的值map成中间信号,同时马上去订阅这个中间信号,之后将中间信号的输出作为新信号的输出。不过flattenMap在bindBlock基础上加入了一些安全检查 (1),因此推荐还是更多的使用flattenMap而非bind。

- (instancetype)flattenMap:(RACStream* (^)(id value))block 
{
    Class class =self.class;

    return[self bind:^{
        return^(id value,BOOL*stop) {
            id stream = block(value) ?: [class empty];
            NSCAssert([stream isKindOfClass:RACStream.class],@"Value returned from -flattenMap: is not a stream: %@", stream); // (1)

            return stream;
        };
    }];
}

(3) map :map操作可将原信号输出的数据通过自定义的方法转换成所需的数据, 同时将变化后的数据作为新信号的输出。它实际调用了flattenMap, 只不过中间信号是直接将mapBlock处理的值返回 (1)。代码与管线图如下。此外,我们常用的filter内部也是使用了flattenMap。与map相同,它也是将filter后的结果使用中间信号进行包装并对其进行订阅,之后将中间信号的输出作为新信号的输出,以此来达到输出filter结果的目的。

- (instancetype)map:(id(^)(id value))block
{
    Class class = self.class;

    return[self flattenMap:^(id value) {
        return[class return:block(value)]; // (1)
    };
}

ReactiveCocoa核心元素与信号流

(4) flatten: 该操作主要作用于信号的信号。原信号O作为信号的信号,在被订阅时输出的数据必然也是个信号(signalValue),这往往不是我们想要的。当我们执行[O flatten]操作时,因为flatten内部调用了flattenMap (1),flattenMap里对应的中间信号就是原信号O输出signalValue (2)。按照之前分析的经验,在flattenMap操作中新信号N输出的结果就是各中间信号M输出的集合。因此在flatten操作中新信号N被订阅时输出的值就是原信号O的各个子信号输出值的集合。这好比将多管线汇聚成单管线,将原信号压平(flatten),如下图所示。

ReactiveCocoa核心元素与信号流

代码如下:

- (instancetype)flatten
{
    return [self flattenMap:^(RACSignal *signalValue) { // (1)
        return [signalValue]; // (2)
    };
}

(5) switchToLatest:与flatten相同,其主要目的也是用于"压平"信号的信号。但与flatten不同的是,flatten是在多管线汇聚后,将原信号O的各子信号输出作为新信号N的输出,但switchToLatest仅仅只是将O输出的最新信号L的输出作为N的输出。用管线图表示如下:

ReactiveCocoa核心元素与信号流

看下代码,当O送出信号A后,新信号N会马上订阅信号A ,但这里用了[A takeUntile O] (1) 。这里的意思就是如果之后原始信号O又送出子信号B,那么之前新信号N对于中间信号A的订阅也就停止了, 如果O又送出子信号C, 那么N又会停止对B的订阅。也就是说信号N订阅的永远都是O派送出来的最新信号。

- (RACSignal*)switchToLatest 
{
    return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACMulticastConnection *connection = [self publish];

        [[connection.signal flattenMap:^(RACSignal *signalValue) {
            return [signalValue takeUntil:[connection.signal concat:[RACSignal never]]]; // (1)
        }] subscribe:subscriber];

        RACDisposable *connectionDisposable = [connection connect];
            return [RACDisposable disposableWithBlock:^{
        }];
    }];
}

另外作为铺垫,这里再提两个操作:

(6) scanWithStart : 该操作可将上次reduceBlock处理后输出的结果作为参数,传入当次reduceBlock操作,往往用于信号输出值的聚合处理。scanWithStart内部仍然用到了核心操作bind。它会在bindBlock中对value进行操作,同时将上次操作得到的结果running作为参数带入 (1),一旦本次reduceBlock执行完,就将结果保存在running中,以便下次处理时使用,最后再将本次得出的结果用信号包装后,传递出去 (2)。

ReactiveCocoa核心元素与信号流

代码如下:

- (instancetype)scanWithStart:(id)startingValue reduceWithIndex:(id(^)(id,id,NSUInteger))reduceBlock 
{
    Class class =self.class;

    return [self bind:^{
        __block id running = startingValue;
        __block NSUIntegerindex = 0;

        return^(id value, BOOL*stop) {
            running = reduceBlock(running, value, index++); // (1)
            return [class return:running]; // (2)
        };
    }];
}

(7) throttle:这个操作接收一个时间间隔interval作为参数,如果Signal发出的next事件之后interval时间内不再发出next事件,那么它返回的Signal会将这个next事件发出。也就是说,这个方法会将发送比较频繁的next事件舍弃,只保留一段“静默”时间之前的那个next事件。这个操作常用于处理输入框等信号(用户打字很快),因为它只保留用户最后输入的文字并返回一个新的Signal,将最后的文字作为next事件参数发出。管线流图表示如下:

ReactiveCocoa核心元素与信号流

前面从代码层面具体剖析了几个RAC Operation。接着我们借着一个特定的需求,试着将这些RAC管线拼凑成一条RAC数据流。假定一个搜索业务如下:用户在searchBar中输入文本,当停止输入超过0.3秒,认为seachBar中的内容为用户的意向搜索关键字searchKey,将searchKey作为参数执行搜索操作。搜索内容可能是多样的,也许包括搜单聊消息、群聊消息、公众号消息、联系人等,而这些信息搜索的方式也有不同,有些从本地获取,有些是去服务器查询,因此返回的速度快慢不一。我们不能等到数据全部获取成功时才显示搜索结果页面,而应该只要有部分数据返回时就将其抛到主线程渲染显示。在这个需求中,从数据输入到最后搜索数据的显示可以具象成一条数据流,数据流中各处对于数据的操作都可以使用上面提到的RAC Operation来完成,通过组合Operation完成以下RAC数据流图:

ReactiveCocoa核心元素与信号流

从数据流图来看,RAC有点类似太极,太极生两仪,两仪生四象,四象生八卦,八卦生万物。我们可以用它的百变性来契合产品的业务需求。按照上面的数据流图,我们可以轻易地写出对应的RAC代码:

[[[self.searchBar rac_textSignal]
throttle:0.3]
subscribeNext:^(NSString*keyString) {
    RACSignal *searchSignal = [self.viewModel createSearchSignalWithString:keyString];

    [[[searchSignal
    scanWithStart:[NSMutableArray array] reduce:^NSMutableArray *(NSMutableArray *running, NSArray *next) {
        [running addObjectsFromArray:next];
        return running;
    }]
    deliverOnMainThread]
    subscribeNext:^(id x) {
        // UI Processing
    }];
}];

可以看到,使用RAC构建数据流后,声明式的代码显得优雅且清晰易读,看似复杂的业务需求在RAC的组织下,一两句代码就得以轻松搞定。反观,如果使用常规方法,估计一个throttle对应的操作就会让逻辑代码散落各处,另一个scanWithStart的对应操作也应该会加入不少中间变量,这些无疑都会大大提升了代码的维护成本。数据流的设计也会让编码者感觉自己更像是代码的设计者,而并非代码的搬运工,让人乐此不疲^_^。

本节内容我们首先从源码层级剖析了几个RAC Operation,相信通过介绍这几个Operation相应的信号衔接细节后,阅读其它的Operation应该不再是什么难事。之后使用RAC数据流处理了一个具体的业务需求。事实上,RAC提供了非常丰富的操作,通过这些操作的组合,我们基本可以处理日常的业务逻辑。当然,需求是多样且奇特的,或许在特定情况下无法找到现成的RAC Operation,因此如果有需要,我们也可以直接拓展RACSignal操作或添加自定义UIKit的RAC拓展,从而让我们的代码 "more functional, more elegant”。可以毫不夸张的说,阻碍RAC发挥的瓶颈只有想象力,当我们接到需求后,仔细推敲数据的走向并设计好相关数据的操作,只要RAC数据流图绘制出来,剩下的代码工作也就信手拈来。

介绍完RAC数据流后,我们再从数据流的角度看看RAC中的另外两个常用元素RACCommand与RACChannel。

RACCommand

RACCommand是RAC很重要的组成部分,通常用来表示某个action的执行。RACCommand提供executionSignals、 executing、 error等一连串公开的信号,方便外界对action执行过程与执行结果进行观察。executionSignals是signal of signals,如果外部直接订阅executionSignals,得到的输出是当前执行的信号,而不是执行信号输出的数据,所以一般会配合flatten或switchToLatest使用。 errors,RACCommand的错误不是通过sendError来实现的,而是通过errors属性传递出来的。 executing,表示该command当前是否正在执行。它常用于监听按钮点击、网络请求等。

使用时,我们通常会去生成一个RACCommand对象,并传入一个返回signal对象的block。每次RACCommand execute 执行操作时,都会通过传入的这个signal block生成一个执行信号E (1),并将该信号添加到RACCommand内部信号数组activeExecutionSignals中 (2),同时将信号E由冷信号转成热信号(3),最后订阅该热信号(4)并将其返回(5)。

- (RACSignal *)execute:(id)input 
{ 
    RACSignal *signal = self.signalBlock(input); //(1)

    RACMulticastConnection *connection = [[signal 
    subscribeOn:RACScheduler.mainThreadScheduler]
    multicast:[RACReplaySubject subject]]; // (3)

    @weakify(self);
    [self addActiveExecutionSignal:connection.signal]; // (2)

    [connection.signal subscribeError:^(NSError *error) {
        @strongify(self);
        [self removeActiveExecutionSignal:connection.signal];
    } completed:^{
        @strongify(self);
        [self removeActiveExecutionSignal:connection.signal];
    }];

    [connection connect]; // (4)

    return [connection.signal]; // (5)
}

以上是RACCommand执行过程,而RACCommand又是如何对执行过程进行监控的呢?

ReactiveCocoa核心元素与信号流

如上图所示,RACCommand内部维护了一个activeExecutionSignals数组。上面提到,每当[RACCommand execute:]后,就会将一个执行信号添加到activeExecutionSignals数组中。RACCommand里设置了两个对activeExecutionSignals的观察信号。第一个观察信号用于监控RACCommand是否正在执行,可以参考上图下端的数据流。activeExecutionSignals是内部执行信号的合集,一旦activeExecutionSignals内部元素发生变化,就会根据执行信号的数量判断RACCommand当前是否正在执行 (1)。

RACSignal *immediateExecuting = [RACObserve(self, activeExecutionSignals) map:^(NSArray *activeSignals) {
    return @(activeSignals.count > 0); // (1)
}];

_executing = [[[[immediateExecuting
deliverOn:RACScheduler.mainThreadScheduler]
startWith:@NO]
distinctUntilChanged]
replayLast];

第二个观察信号用于监控RACCommand当前正在执行的信号与信号产生的error,可以参考上图上端数据流。每当activeExecutionSignals有新的执行信号添加进数组,newActiveExecutionSignals就会有相应的信号输出(信号newActiveExecutionSignals输出的是信号,因此newActiveExecutionSignals是信号的信号)。由于newActiveExecutionSignals之后需要转成executionSignals、error信号,并分别被外界订阅,为避免产生多余的副作用,这里使用publish将activeExecutionSignals对应的观察信号由冷信号转成了热信号(1)。之后executionSignals将newActiveExecutionSignals的输出值抛送到主线程上 (2)。当我们去订阅executionSignals信号时,拿到的就是当前正在执行的信号。要是我们关心的是当前执行信号的输出值,我们得使用 [executionSignals flatten]方法(参考上节的flatten操作)将executionSignals”压平”后,才可以获取到所有当前执行信号的输出值。

RACSignal *newActiveExecutionSignals = [[[[[self
rac_valuesAndChangesForKeyPath:@keypath(self.activeExecutionSignals) options:NSKeyValueObservingOptionNew observer:nil]
reduceEach:^(id _, NSDictionary *change) {
    NSArray *signals = change[NSKeyValueChangeNewKey];
    return [signals.rac_sequence signalWithScheduler:RACScheduler.immediateScheduler];
}]
concat]
publish] // (1)
autoconnect];

_executionSignals = [[newActiveExecutionSignals
map:^(RACSignal *signal) {
    return [signal catchTo:[RACSignal empty]];
}]
deliverOn:RACScheduler.mainThreadScheduler]; // (2)

同时如果执行信号抛出了错误,newActiveExecutionSignals通过flattenMap,直接将产生的错误包装成错误信号抛往主线程,并通知订阅者。

RACMulticastConnection *errorsConnection = [[[newActiveExecutionSignals
flattenMap:^(RACSignal *signal) {                                              
    return [[signal
    ignoreValues]
    catch:^(NSError *error) {
        return [RACSignal return:error];
    }]
}]
deliverOn:RACScheduler.mainThreadScheduler]
publish];

_errors = [errorsConnection.signal setNameWithFormat:@"%@ -errors", self];
[errorsConnection connect];

因此,RACCommand主要是对成员变量activeExecutionSignals数组的变化进行观察, 并将观察结果转变成外部感兴趣的信号,从而使得RACCommand的执行过程与结果可被外部监控。我们往往将RACCommand与UI响应配合使用,比如在button被点击后,去执行一个网络请求的command。我们可以通过command.executing信号输出的信号值决定是否弹出小菊花,可以通过command.executionSignals信号获取当前正在执行的信号,并得到执行结果,也可以从command.error信号中拿到我们需要反馈给用户的错误提示信息,使用起来十分方便。

RACChannel

RACChannel可能相对来说比较陌生,但它也可以在信号流中扮演重要的角色。它提供双向绑定功能,一个RACChannel的两端配有RACChannelTerminal,分别是leadingT、 followingT。我们可以将leadingT 与 followingT想象成一根水管的两头,只要任何一端输入信号值,另外一端都会有相同的信号值输出。有了这个概念下我们再来看看RACChannelTerminal。首先

@interface RACChannelTerminal : RACSignal <RACSubscriber>

可以发现RACChannelTerminal因为继承了RACSignal, 因此它具有信号的特性,可以被订阅。比如:在RACChannel中 [leadingT subscribeNext:],这里leadingT扮演的就是signal的角色,当它被订阅时输出的就是followingT送出的值。同时RACChannelTerminal又实现了RACSubscriber的协议。这样就意味着它又能够像订阅者一样调用sendNext: sendError: sendComplete方法。 如果followingT被订阅了,那么一旦leadingT sendNext:value,信号值value就会穿过leadingT与followingT,被followingT的订阅者捕获到。正是由于RACChannelTerminal拥有这种既可被订阅,又可主动输出信号值的属性,当它被放到RACChannel两端时,就可让两端信号相互影响。

通常我们很少直接使用RACChannel,最常用到的就是RACChannelTo,下面我们来详细了解下:

ReactiveCocoa核心元素与信号流

借着上面的RACChannelTo的数据流图,我们拿RAC提供的示例代码举例。RACChannelTo宏实际生成了一个RACKVOChannel,RACKVOChannel内部是将其一端的leadingT与相关keypath上的integerProperty绑定,并将其另外一端followingT(对应示例代码中的integerChannelT)暴露出来。当我们拿到integerChannelT后,使用[integerChannelT sendNext:@“5”] (1), 信号值就会传到RACKVOChannel的另一端,影响integerProperty(参考图中红色管线)。同时当integerChannelT被订阅时,只要另一端integerProperty因变化产生了对应信号值A,那么integerChannelT就会将信号值A传递給它的订阅者(参考图中蓝色管线)。

RACChannelTerminal *integerChannelT = RACChannelTo(self, integerProperty, @42);
[integerChannelT sendNext:@5]; // (1)

[integerChannelT subscribeNext:^(id value) { // (2)
    NSLog(@"value: %@", value);
}];

事实上,RAC为很多类提供了RACChannel相关的拓展,如

  • [NSUserDefaults rac_channelTerminalForKey:]
  • [UIDatePicker rac_newDateChannelWithNilValue:]
  • [UISegmentedControl rac_newSelectedSegmentIndexChannelWithNilValue:]
  • [UISlider rac_newValueChannelWithNilValue:]
  • [UITextField rac_newTextChannel:]

这些函数都会返回一个对应的RACChannelTerminal。有了这个RACChannelTerminal,一方面我们可以通过它去观察对应控件内核心变量的变化情况,并作出响应。另一方面我们也可通过这个RACChannelTerminal直接去改变这个控件里的核心变量。比如我们可以使用[UITextField rac_newTextChannel:]返回的RACChannelTerminal用以下方式实现控件与viewModel中数据的双向绑定。

RACChannelTerminal *textFieldChannelT = textField.rac_newTextChannel;
RAC(self.viewModel, property) = textFieldChannelT;
[RACObserve(self.viewModel, property) subscribe:textFieldChannelT];

整体而言,RACChannelTerminal用起来十分顺手,如果契合业务使用,RACChannel能够提供非常大的价值。

总结

本文从源码层面剖析了RAC信号的订阅过程,介绍了RAC核心元素在其中扮演的角色。之后着重介绍RAC数据流构建与它的使用价值。本文没有对所有的RAC Operation进行覆盖性的介绍,而是挑出了几个重要的Opration,借助源码与数据流图介绍其内部运作细节,希望能从底层阐述构建原理,帮助大家更好的理解使用RAC。就如一句老话所说"开车不需要知道离合器是怎么工作的,但如果知道离合器原理,那么车子可以开得更平稳"。

不想错过技术博客更新?想给文章评论、和作者互动?第一时间获取技术沙龙信息?

请关注我们的官方微信公众号“美团点评技术团队”。现在就拿出手机,扫一扫:

ReactiveCocoa核心元素与信号流

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址