ReactiveCocoa框架菜鸟入门——信号(Signal)与订阅者(Subscriber)

上一篇文章已经简单的介绍了ReactiveCocoa框架的思想和优势。本文初步研究一下ReactiveCocoa框架的使用方法。

写在开始前

传统的编程思想,大概是用户产生某个事件,然后得到相应的参数,传入事先已经实现的方法中,处理完成后把结果在UI界面上反馈出来。ReactiveCocoa框架中大量的使用了block,这意味着,很多block内的代码,是在将来某一个合适的时刻被执行的。如果你看到block里某个参数并没有被赋值,也没有传入参数,不要奇怪,程序运行到这里的时候还不会执行这个block,至于等到需要执行block的时候,会有参数传入的。这是新手在使用block时非常容易产生的误区,如果没有理解这一点,在看代码的时候会产生相当大的麻烦。

信号(Singal)

ReactiveCocoa最基本也是最关键的一个概念叫做信号(Signal)。官方给出的文档中对于信号如此定义:

[RACSignal] is a push-driven stream with a focus on asynchronous event

delivery through subscriptions.

当然这个解释是非常抽象的,一会儿再谈。

个人感觉,linyawen的博客中对信号的解释非常生动形象

信号是数据流,可以被绑定和传递。可以把信号想象成水龙头,只不过里面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况)。水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开。这样只要有新的玻璃球进来,就会自动传送给接收方。

请牢牢记住“信号就像一个水龙头”这个概念。它允许各种各样的数据从它这里流过。一个信号可以捕捉当前和未来的值。

订阅者/接收者(Subscriber)

回到最初的官方给出的signal的定义,push-driven stream表示,signal是一个由推动力驱动的数据流。即产生一个数据就push一次并进行一系列处理。反过来说,pull-driven表示处理完一个数据才会要求产生一个数据。

对于一个signal来说,刚刚创建的时候,它还是一个冷信号(Cold signal),只有在有了订阅者(Subscriber)之后,才会变为热信号(Hot signal)。订阅者就好比水龙头最下方的水盆,只有放好了水盆,水龙头才能打开。不然,水(value)不都浪费了么?

如果目前对于信号和订阅者还不了解,这是正常情况。接下来通过代码一起了解他们的工作机制。

初窥ReactiveCocoa

ReactiveCocoa的框架的安装已经在前文谈过,这里不再细讲。具体操作方法参见这里

创建一个空白的工程,从storyboard里面拖入一个UITextField,不妨命名为searchText。

在viewDidLoad方法里面添加以下代码:

[self.searchText.rac_textSignal subscribeNext: ^(id text){
        NSLog(@"%@", text);
    }];

运行一下程序,在textfield中输入“hello”。输出结果应该是这样的

2015-05-28 14:55:57.785 TwitterInstant[716:114961] h
2015-05-28 14:55:58.135 TwitterInstant[716:114961] he
2015-05-28 14:55:58.303 TwitterInstant[716:114961] hel
2015-05-28 14:55:58.415 TwitterInstant[716:114961] hell
2015-05-28 14:55:58.663 TwitterInstant[716:114961] hello

可以发现,每一次textfield中文字发生变化,都执行了block中的代码。

一切脱离原理讲效果的行为都是耍流氓。相信任何第一次接触这个框架的读者看到这里一定是一头雾水。很可能你会问这些问题:

  1. 为什么参数text就是textfield中的内容而不是其他的任何信息?
  2. 为什么每次textfield中内容发生变化,block都被执行?
  3. subscribeNext方法是什么意思?
  4. 前文所说的信号、订阅者具体指什么?

下面就是时候看看具体的原理了。千万不要着急学习更多的知识,有时候打好基础,彻底理解一个框架的原理对以后的深入学习更有帮助。请记住这四个问题,如果你能不假思索的回答出来,那么就可以开始进一步的学习了。

ReactiveCocoa的基本工作原理

不妨把上述代码自己敲一遍。searchText后面输入.r的时候已经会出现Xcode的智能提示了。

这里的rac_textSignal就是一个信号。执行了subscribeNext方法后,这个信号被订阅(水龙头下面摆好水盆了)。每当有新的数据(textfiled中内容)出现,信号就捕捉到这个值。把这个值传入block中,并且执行block里的代码,于是就打印出了我们看到的数据。为了搞懂为什么新数据出现时,block被调用,我们需要看看subscribeNext方法的具体实现。

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    NSCAssert(NO, @"This method must be overridden by subclasses");
    return nil;
}

- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock {
    NSCParameterAssert(nextBlock != NULL);

    RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
    return [self subscribe:o];
}

- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock completed:(void (^)(void))completedBlock {
    NSCParameterAssert(nextBlock != NULL);
    NSCParameterAssert(completedBlock != NULL);

    RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:completedBlock];
    return [self subscribe:o];
}

这里从源代码中简单的节选了几个方法的实现。不难发现,subscribeNext是一个方法簇,除了不带block的那个方法(第一个方法)之外,每一个方法中都创建了一个RACSubscriber(订阅者)对象。subscribeNext方法返回一个RACDisposable(销毁者)对象,这个对象可以用来销毁一个信号,大部分时候没有必要这么做。因为没有被持有的信号会被自动释放。

看到这里,有一个问题的答案已经很明显了:谁是订阅者(subscriber)?

很显然,我们不可能像找到signal那样说rac_textSignal就是一个信号对象,但是订阅者对象就藏在subscribeNext方法中。任何简化的不完全的subscribeNext方法都可以拓展成一个完整的subscribeNext方法:

self.searchText.rac_textSignal subscribeNext:^(id x) {
        <#code#>
    } error:^(NSError *error) {
        <#code#>
    } completed:^{
        <#code#>
    }

而这里我们指定的三个block将会被用来构建一个订阅者(subscriber)。

信号(Signal)和订阅者(Subscriber)

还记得第一个demo的四个问题么?回想一下,是不是已经解决问题三和四了呢?为了弄明白前两个问题,我们来看一看信号的工作原理。rac_textSignal是一个已经被创建好的信号,为了不是一般性,我们自己创建一个信号:使用createSignal方法。

- (RACSignal *)createSignal{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"signal created");
        return nil;
    }];
}

有了这个方法,我们就可以创建一个最简单的自定义的信号了。在viewDidLoad方法中加入这样一行代码:

RACSignal *signal = [self createSignal];

运行程序看一看效果吧。并没有什么卵用。。。。。。。这是必然的。说得形象一点,我们搞出来一个水龙头,但是下面还没有放上盆子。说得专业一点,这还是一个冷信号(Cold signal)。还没有被订阅(Subscribe)。归根结底,我们不了解这个createSignal方法的实现原理。

Ok,按住command键点击方法名,跳过去看看createSignal到底是个啥玩意儿。

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
    return [RACDynamicSignal createSignal:didSubscribe];
}

这个方法有一个block。block需要一个RACSubscriber(订阅者)对象作为传入的参数,会传出一个销毁者(RACDisposable)对象。block名叫做didSubscribe,显然这个block会在block被subscribe之后触发。现在明白冷信号是什么意思了么?没有订阅者(Subscriber)的时候,用于创建signal的block都没有被执行,这个信号当然没有任何卵用。

在viewDidLoad方法中再加一行代码呢:

[signal subscribeNext:^(id x) {
        ;
    }];

即使只是一个空的block,但是因为signal现在被订阅(Subscribe)了,之前的名为didSubscribe的block就被执行了。此时会输出

2015-05-28 15:36:42.661 TwitterInstant[823:253903] signal created

修改一下刚刚的代码,增加一个NSLog的功能。修改后的subscribeNext方法应该像这样:

RACSignal *signal = [self createSignal];
    [signal subscribeNext:^(id x) {
        NSLog(@"aaa");
    }];

运行程序。Oh,Shit!依然没有任何卵用。说好的会执行subscribeNext的block内的代码呢?aaa怎么没有打出来?

其实,仔细一想,这样的结果反而是合理的。我们之前讨论的信号(Signal)被订阅(Subscribe)之后,信号这个水龙头的阀门被打开,水流过信号,落入盆子里。可是为啥落入盆子里就会调用subscribeNext的block中的代码?文章的最初写了,block只是一段预先定义好的代码,在将来的某个时间被调用。可我们至今没有在任何代码中看到这样的调用。我们还是再好好看一看订阅者(Subscriber)吧。

细说订阅者(Subscriber)

之前我们简单的看了订阅者(Subscriber)对象的工作原理:在subscribeNext方法中我们创建了一个订阅者(Subscriber)对象。这个对象是怎么被创建的呢?关键在于那个出现了无数次的subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed方法:

+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed {
    RACSubscriber *subscriber = [[self alloc] init];

    subscriber->_next = [next copy];
    subscriber->_error = [error copy];
    subscriber->_completed = [completed copy];

    return subscriber;
}

看到这里,我想已经非常清楚了。订阅者(Subscriber)对象持有三个属性,都是block。在订阅者的创建过程中,对这三个进行了赋值。等待在将来的某一刻被调用。这就好比这个盆子(订阅者)有许多功能,对于不同的滴水状态,具备不同的处理方法。

同时订阅者(Subscriber)对象还对外提供了三个方法的调用接口:

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

以- (void)sendNext:(id)value方法为例看一看它的实现:

- (void)sendNext:(id)value {
    @synchronized (self) {
        void (^nextBlock)(id) = [self.next copy];
        if (nextBlock == nil) return;

        nextBlock(value);
    }
}

其实最核心的功能即时调用了自己的nextBlock并传入相应的参数而已。

那么订阅者(Subscriber)对象的这三个方法什么时候被调用呢?又应该调用哪一个呢?答案是:由信号决定。试想一下:水龙头告诉底下的盆子:“我要滴水了,你存一下吧。”。这就是让订阅者(盆子)执行nextBlcok。如果水龙头说:“我滴完水了,你可以歇一会了。”这就是让订阅者(盆子)执行completeBlock。如果水龙头说:“我滴不了水了,坏掉了,你可以歇一会了。”这就是让订阅者(盆子)执行errorBlock。显然,在一个信号(Signal)的生命周期中,可以发送无数次next事件,和唯一一次complete或者error事件。

改造一下最初创造signal的方法:

- (RACSignal *)createSignal{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"signal created");
        [subscriber sendNext:nil];
        return nil;
    }];
}

再次运行,就会发现aaa已经被打印出来了。关键就在于这一次我们调用了订阅者(Subscriber)的sendNext方法。

说了这么多,回头看看问题一。重点在于这个rac_textSignal。如果有兴趣可以看看它的实现。其实本质上还是监听了textfield的text属性。当它发生变化时,就把它的值传出来。至于为什么每次传出的都是text值,这是由这个signal的创建方法决定的。

至于问题二,我们执行了subscribeNext方法创建了一个订阅者(Subscriber),这个订阅者的nextBlcok方法已经被赋值。而rac_textSignal这个信号的实现中,在每次text发生变化的时候,就会调用订阅者的sendNext方法,从而调用nextBlcok中的代码。

这就是一个信号(Signal)与订阅者(Subscriber)的简单实用。当然,ReactiveCocoa框架的功能远不止这些。下一章,我们来讨论一下更多的关于信号(Signal)的使用方法。

时间: 2024-10-01 05:01:16

ReactiveCocoa框架菜鸟入门——信号(Signal)与订阅者(Subscriber)的相关文章

ReactiveCocoa框架菜鸟入门——信号(Signal)详解

基础知识 在阅读本文之前,请确保你已成功导入ReactiveCocoa框架并对信号(Signal)和订阅者(Subscriber)有基本了解.或者尝试着完全理解以下一段内容: 信号是数据流,可以被绑定和传递.可以把信号想象成水龙头,只不过里面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况).水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开.这样只要有新的玻璃球进来,就会自动传送

Bootstrap框架菜鸟入门教程

Bootstrap菜鸟入门教程 Bootstrap简介 Bootstrap,来自 Twitter,是目前最受欢迎的前端框架.Bootstrap 是基于 HTML.CSS.JAVASCRIPT 的,它简洁灵活,使得 Web 开发更加快捷. 一.栅格系统 栅格系统的工作原理: "行(row)"必须包含在 .container (固定宽度)或 .container-fluid (100% 宽度)中,以便为其赋予合适的排列(aligment)和内补(padding). 通过"行(ro

迷你MVVM框架 avalonjs 入门教程(司徒正美)

迷你MVVM框架 avalonjs 入门教程 关于AvalonJs 开始的例子 扫描 视图模型 数据模型 绑定属性与动态模板 作用域绑定(ms-controller, ms-important) 模板绑定(ms-include) 数据填充(ms-text, ms-html) 类名切换(ms-class, ms-hover, ms-active) 事件绑定(ms-on,……) 显示绑定(ms-visible) 插入绑定(ms-if) 双工绑定(ms-duplex) 样式绑定(ms-css) 数据绑

[HMLY]14.ReactiveCocoa 和 MVVM 入门

MVC 任何一个正经开发过一阵子软件的人都熟悉MVC,它意思是Model View Controller, 是一个在复杂应用设计中组织代码的公认模式. 它也被证实在 iOS 开发中有着第二种含义: Massive View Controller(重量级视图控制器).它让许多程序员绞尽脑汁如何去使代码被解耦和组织地让人满意. 总的来说, iOS 开发者已经得出结论: 他们需要给视图控制器瘦身, 并进一步分离事物;但该怎么做呢? MVVM 于是MVVM流行起来, 它代表Model View View

【长篇高能】ReactiveCocoa 和 MVVM 入门

翻译自ReactiveCocoa and MVVM, an Introduction. 文中引用的 Gist 可能无法显示.为了和谐社会, 请科学上网. MVC 任何一个正经开发过一阵子软件的人都熟悉MVC,它意思是Model View Controller, 是一个在复杂应用设计中组织代码的公认模式. 它也被证实在 iOS 开发中有着第二种含义: Massive View Controller(重量级视图控制器).它让许多程序员绞尽脑汁如何去使代码被解耦和组织地让人满意. 总的来说, iOS

ReactiveCocoa 和 MVVM 入门

翻译自ReactiveCocoa and MVVM, an Introduction. 文中引用的 Gist 可能无法显示.为了和谐社会, 请科学上网. MVC 任何一个正经开发过一阵子软件的人都熟悉MVC,它意思是Model View Controller, 是一个在复杂应用设计中组织代码的公认模式. 它也被证实在 iOS 开发中有着第二种含义: Massive View Controller(重量级视图控制器).它让许多程序员绞尽脑汁如何去使代码被解耦和组织地让人满意. 总的来说, iOS

ReactiveCocoa 和 MVVM 入门 (转)

翻译自ReactiveCocoa and MVVM, an Introduction. 文中引用的 Gist 可能无法显示.为了和谐社会, 请科学上网. MVC 任何一个正经开发过一阵子软件的人都熟悉MVC,它意思是Model View Controller, 是一个在复杂应用设计中组织代码的公认模式. 它也被证实在 iOS 开发中有着第二种含义: Massive View Controller(重量级视图控制器).它让许多程序员绞尽脑汁如何去使代码被解耦和组织地让人满意. 总的来说, iOS

Android轻量级ORM框架ActiveAndroid入门教程(转)

注:没有找到出处,如有侵犯,请告知 开始ActiveAndroid神奇之旅: 在AndroidManifest.xml中我们需要添加这两个 AA_DB_NAME (数据库名称,这个name不能改,但是是可选的,如果不写的话 是默认的"Application.db"这个值) AA_DB_VERSION (数据库版本号,同样是可选的 – 默认为1) <manifest ...> <application android:name="com.activeandro

开源框架Pushlet入门_java_web

开源框架Pushlet入门 一.comet基本概念 1.comet是一个用于描述客户端和服务器之间交互的术语,即使用长期保持的http连接来在连接保持畅通的情况下支持客户端和服务器间的事件驱动的通信. 2.传统的web系统的工作流程是客户端发出请求,服务器端进行响应,而comet则是在现有技术的基础上,实现服务器数据.事件等快速push到客户端,所以会出现一个术语"服务器推"技术. 二.push实现方式 1.原理: 利用jsp/servel技术,在不关闭http流的情况下push数据到