ReactiveCocoa 学习笔记(一)之 RACSignal

It's always hard to start, but after that, all you need is persistence.

这已经是第二次看 ReactiveCocoa 了,上次是几个月之前,虽然上次没做什么总结,但是回顾了几天后收获还是很大,这次就趁热把脑袋的还热乎的想法记录下来供以后参考。

我这里就不再去定义 RAC 是什么,需要了解基础的去看看学习 RAC 必看的几篇文章:

注:我目前看的还是 ReactiveCocoa v2.5 版本。

RACStream

RACStream 是基于函数式编程中的 Monad 的概念建立的,可以说它是整个 RAC 的基石,有了它才得以实现 RAC 中的流式和链式编程。

在这个类中,最重要的就是 bind 这个函数,为了便于理解,我们可以看看 haskell 中 Monad 中的对 bind 的函数定义:

it takes a monadic value (that is, a value with a context) and feeds it to a function that takes a normal value but returns a monadic value, and return another monadic value.

翻译过来就是这个函数接收一个带有上下文的值,在它上面应用一个接收普通值但返回带有上下文的值的函数,最后返回另一个带有上下文的值。

这里我们用 Swift 来模拟这个函数的签名:

func bind(a: Monad<T>, Int -> Monad<T>) -> Monad<T>

但是你会发现这和 RAC 中 bind 的签名并不相同:

typedef RACStream * (^RACStreamBindBlock)(id value, BOOL *stop);
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;

相反倒是和 flatmap 的签名很像。

那么 RAC 中的 bind 函数是干嘛的呢?里面的原理理解起来很复杂,我们只需要知道的是,在 RACStream 的 operation 这个分类中,很多例如 flattenMap、scanWithStart 等函数都是直接或者间接通过 bind 来构建,这些函数不仅能够链式调用,还把‘值流’这个概念贯穿起来。

要彻底的理解 bind 函数的作用感觉不太容易,我感觉也没有太大的必要去画太多的时间去研究它,了解函数式操作的那些函数的具体的意义和作用反而更有实用价值。

RACSignal 和 RACSubscriber

通常来说,我们并不会直接用到 RACStream,而是用它的一个重要的子类 RACSignal。

为了说清楚这一部分,我们来先看几个基本的概念:

首先是 action,这是我自己添加的一个概念,action 可以是点击按钮、访问网络或者打开文件等等动作,动作必然会产生 event,事件中包含相应的结果值,RAC 中有三类事件,分别为 next、error 和 complete,next 表示事件成功,可以继续进行下一步操作,error 表示事件失败,complete 表示事件完成。

接下来就是 signal 信号,这个概念比较抽象,官方的解释是:

Signals generally represent data that will be delivered in the future.

信号代表着将在未来传递的值,这个值就是事件包含的值,信号可以看做是‘值流’的源头。

如果你想要拿到这些值,就必须订阅信号,成为订阅者。

信号是 push-driven 的,也就是说值是一个一个的推送给订阅者,订阅者无法干预或主动获取想要的值,不仅如此,信号在传递值流的过程中,还可以产生副作用。

如果一个信号只有在每次被订阅之后才开始传递值并产生副作用,那么这样的信号称为‘冷信号’,相反,如果信号的值传递和订阅者没关系,那么这个信号就被称为‘热信号’。

创建一个信号很简单:

RACSignal *loginSigal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    
    [self loginWithSuccess:^(id userInfo) {
        [subscriber sendNext:userInfo];
        [subscriber sendCompleted];
    } failed:^(NSError *error) {
        [subscriber sendError:error];
    }];
    
    return nil;
}];

[[[loginSigal doNext:^(id x) {
    // 副作用
}] map:^id(id value) {
    // 转换值
    return [value description];
}] subscribeNext:^(id x) {
    NSLog(@"%@", x);
} error:^(NSError *error) {
    NSLog(@"%@", error);
}];

createSignal 方法接收一个叫 didSubscribe 的 block,这个 block 接收一个 subscriber 并返回一个 disposable,关于这个 disposable 我们稍后再说。

可以看到,在这个 didSubscribe block 里面,先发起来一个登陆请求,在成功的回调中,给 subscriber 发送 sendNext 消息,然后再发送 sendCompleted 消息,在失败的回调中,发送 sendError 消息。

一开始看起来是比较奇怪的,因为发送值的工作竟然是由 subscriber 做,感觉跟订阅者的定义有冲突,后来我想了一下,这应该是借鉴了 OC 发送消息的思想,这里的 sendNext 相当于是给订阅者发送 next value,而不应该看作是调用 sendNext 方法。在 RAC 中,通常来说我们并不会去实例化一个 subscriber,我们只能通过 subscribeNext 等方法来订阅信号,而信号内部会帮我们创建 subscriber 来完成相应的工作。

通过阅读源码可以发现,RACSignal 其实是通过子类 RACDynamicSignal 来完成创建信号的工作的。RACDynamicSignal 内部有一个 didSubscribe 的 block 属性,createSignal 传入的 block 的一份 copy 被赋值给这个属性保存起来。

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

接下来,当每一次我们给 signal 发送以 subscribe 开头的消息时,signal 会首先创建一个 subscriber 实例,并且把相应的 block (nextBlock、errorBlock...)赋值给 subscriber 相应的属性,

- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock {
    NSCParameterAssert(nextBlock != NULL);
    
    // 创建一个订阅者,并把 nextBlock 保存下来
    RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];
    return [self subscribe:o];
}

然后调用了一个非常重要的方法:

- (RACDisposable *)subscribe (id<RACSubscriber>)subscriber;

这个方法还是由 RACDynamicSignal 来实现的,抛开其他的,在这个方法里调用了最开始我们传入的那个 didSubscribe block,这时候信号才执行。

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    NSCParameterAssert(subscriber != nil);

    RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
    subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable];

    if (self.didSubscribe != NULL) {
        RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{
          // 这里调用了之前的 didSubscribe,是作为属性保存下来的。
            RACDisposable *innerDisposable = self.didSubscribe(subscriber);
            [disposable addDisposable:innerDisposable];
        }];

        [disposable addDisposable:schedulingDisposable];
    }
    
    return disposable;
}

拿上面的代码举例,这时候才开始执行 loginWithSuccess,成功后执行 subscriber 的 sendNext 方法,这个方法会调用 subscribeNext 传入保存的的 nextBlock。

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

        nextBlock(value);
    }
}

这个值在传到 nextBlock 之前我们还可以对它做不少的操作和副作用,这应该就是 RACStream 中 bind 函数的功劳了。具体如何实现的不深究了,因为实在看不懂。

总结

到这里先结束了,了解了一些基础和思想后,接下来的笔记可能更关心 RAC 中各个组成成分的用法,让我们投入到实战吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容