在这个类别中,将信号的 next:、error: 和 completed 以及这三个事件的组合都以 block 的形式封装起来,从以上代码中可以看出,这些方法最终调用的还是 - (void)nl_subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock; 方法,而它则封装了订阅者 NLSubsciber。
通过这么个小小的封装,客户使用起来就极其方便了:
*@brief创建一个自定义的信号。
*这个信号在被订阅时,会发送一个当前的日期值;
*再过三秒后,再次发送此时的日期值;
*最后,再发送完成事件。
*/
RACSignal*signalInterval=[RACSignalcreateSignal:^(idsubscriber){
[subscribersendNext:[NSDatedate]];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(3.0*NSEC_PER_SEC)),dispatch_get_main_queue(),^{
[subscribersendNext:[NSDatedate]];
[subscribersendCompleted];
});
(id)nil;
}];
[signalIntervalnl_subscribeNext:^(idx){
}error:^(NSError*error){
"error:%@"
}completed:^{
);
}];
输出如下:
2015-08-1623:29:44.406RACPraiseDemo[653:32675]next:2015-08-1615:29:44+0000
2015-08-1623:29:47.701RACPraiseDemo[653:32675]next:2015-08-1615:29:47+0000
2015-08-1623:29:47.701RACPraiseDemo[653:32675]completed
本例并没有采用之前的 “定时器信号”,而是自己创建的信号,当有订阅者到来时,由这个信号来决定在什么时候发送什么事件。这个例子里发送的事件的逻辑请看代码里的注释。
看到这里,是不是很熟悉了?有没有想起 subscribeNext:,好吧,我就是在使用好多好多次它之后才慢慢入门的,谁让 RAC 的大部分教程里面第一个讲的就是它呢!
到了这里,是不是订阅者这部分就完了呢?我相信你也注意到了,这里有几个不对劲的地方:
a.无法随时中断订阅操作。想想订阅了一个无限次的定时器信号,无法中断订阅操作的话,定时器就是永不停止的发下去。
b.订阅完成或错误时,没有统一的地方做清理、扫尾等工作。比如现在有一个上传文件的信号,当上传完成或上传错误时,你得断开与文件服务器的网络连接,还得清空内存里的文件数据。
再探FlattenMap与Map
问题提出
有时候,我们需要把一个异步的API用信号的方式来表示。比如,点击登录按钮后异步的访问服务器,当获取到数据的时候再调用订阅者的处理方法。一个可能会出现的代码大概是这样:
- (RACSignal *)signInSignal {
return [RACSignal createSignal:^RACDisposable *(id subscriber){
[self.signInService
signInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text
complete:^(BOOL success){
[subscriber sendNext:@(success)];
[subscriber sendCompleted];
}];
return nil;
}];
}
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
map:^id(id x){
return [self signInSignal];
}]
subscribeNext:^(id x){
NSLog(@"Sign in result: %@",x);
}];
这样的代码并不能正常运行,原文给出的解决方案是把map方法换成flattenMap方法,但是却没有给出解释。在此专栏的上一篇文章,我们已经研究过了flattenMap的工作原理,并且明确了一点:map方法是基于flattenMap方法实现的。
再探FlattenMap与Map
在了解这两个方法的工作原理之前,我们不妨梳理一下思路。对于一个信号流(由若干个信号前后拼接而成)中的每一个信号而言,我们最关心它能传递出什么数据,就像我们只关心水龙头里面流出的是水还是石油一样。至于流入的数据,一定是上一个信号的流出数据。
然而,在之前的讨论中我们已经清楚,信号能流出什么样的数据,是在创建这个信号的决定的。也就是说,订阅者拿到的数据,取决于在创建信号的时候,我们制定的sendNext方法的参数。与现实生活不同的是,我们可以在水龙头之间添加过滤网来实现类似的功能。
在一文中,我们已经详细的查看了bind方法和flattenMap方法的代码和文档。
flattenMap方法,实际上是根据前一个信号传递进来的参数重新建立了一个信号,这个参数,可能会在创建信号的时候用到,也有可能根本用不到。比如在之前的例子中,我们其实调用了自定义的方法来创建信号。
之前提到过,我们关注一个信号能传递什么数据出来,那么调用了flattenMap方法创建的信号,会传出什么样的值呢?
答案是不知道!!!
因为flattenMap方法并不关心生成的信号会传递什么值,它只负责
根据前一个信号的参数创建一个新的信号!
而至于这个信号会传递什么值,之前也提到过,是在创建信号的时候指定的。比如本文所举的例子中,我们自定义的信号会传递这样的值
[subscriber sendNext:@(success)]
理解了这一点之后,map方法就简单了。先看一下map方法的定义:
- (instancetype)map:(id (^)(id value))block {
NSCParameterAssert(block != nil);
Class class = self.class;
return [[self flattenMap:^(id value) {
return [class return:block(value)];
}] setNameWithFormat:@"[%@] -map:",self.name];
}
这里的[class return:block(value)]方法实现了flattenMap方法的功能,他返回了一个信号。return:方法的代码实现有点长,有兴趣的读者可以自行查阅,这里就不具体分析,总结来说就是,这个信号传递的值讲是block(value)。
flattenMap方法和map方法都有一个带参数value的block作为这个方法的参数。不同的是,flattenMap方法通过调用block(value)来创建一个新的方法。它可以灵活的定义新创建的信号。而map方法,将会创建一个和原来一模一样的信号,只不过新的信号传递的值变为了block(value)。
总结一下,个人对map的理解是“变换”。map方法,根据原信号创建了一个新的信号,并且变换了信号的输出值。这两个信号具有明显的先后顺序关系。而flattenMap方法,直接生成了一个新的信号,这两个信号并没有先后顺序关系,属于同层次的平行关系。这也许就是为什么会被命名为flattenMap吧。
实践检验
回头看一看此前的例子之所以用map方法不行的原因在于,map方法创建的信号,接收了前一个信号传递的值,传出的值是[self signInSignal]的执行结果,即任然是一个信号,但是我们并不需要这个信号,我们需要的是这个信号的传出值。
使用flattenMap方法就可行的原因在于,flattenMap方法生成了一个新的信号,也就是我们调用[self signInSignal]的执行结果。这个信号的传出的值,在信号的创建过程中已经被定义。所以可以正常工作。
吐槽一句
非常感谢最初翻译出ReactiveCocoa教程的大牛,给了我们快速入门的机会。但是能力越大,责任也越大,简单一句flattenMap可以处理信号中的信号,是非常不负责任的,可能会影响无数的学习者。甚至原作者当时也没能完全理解flattenMap的工作原理以及和map方法之间的关系。我想,严谨、求真知,是对任何一门科学的基本尊重。包括软件工程。
这句话,也送给未来的自己!
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!