细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信
前一篇文章我们介绍了冷信号与热信号的概念,可能有同学会问了,为什么RAC要搞得如此复杂呢,只用一种信号不就行了么?要解释这个问题,需要绕一些圈子。 前面可能比较难懂,如果不能很好理解,请仔细阅读相关文档。 最前面提到了RAC是一套基于Cocoa的FRP框架,那就来说说FRP吧。FRP的全称是Functional Reactive Programming,中文译作函数式响应式编程,是RP(Reactive Programm,响应式编程)的FP(Functional Programming,函数式编程)实现。说起来很拗口。太多的细节不多讨论,我们着重关注下FRP的FP特征。 FP有个很重要的概念是和我们的主题相关的,那就是纯函数。 纯函数就是返回值只由输入值决定、而且没有可见副作用)的函数或者表达式。这和数学中的函数是一样的,比如:
这个函数在调用的过程中除了返回值以外的没有任何对外界的影响,除了入参x以外也不受任何其他外界因素的影响。 那么副作用都有哪些呢?我来列举以下几个情况:
由此我们可以看出,在目前的iOS编程中,我们是很难摆脱副作用的。甚至可以这么说,我们iOS编程的目的其实就是产生各种副作用。(基于用户触摸的外界因素,最终反馈到网络变化和屏幕变化上。) 接下来我们来分析副作用与冷热信号的关系。既然iOS编程中少不了副作用,那么RAC在实际的使用中也不可避免地要接触副作用。下面通过一个业务场景,来看看冷信号中副作用的坑: self.sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://api.xxxx.com"]]; self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer]; self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer]; @weakify(self) RACSignal *fetchData = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { @strongify(self) NSURLSessionDataTask *task = [self.sessionManager GET:@"fetchData" parameters:@{@"someParameter": @"someValue"} success:^(NSURLSessionDataTask *task,id responSEObject) { [subscriber sendNext:responSEObject]; [subscriber sendCompleted]; } failure:^(NSURLSessionDataTask *task,NSError *error) { [subscriber sendError:error]; }]; return [RACDisposable disposableWithBlock:^{ if (task.state != NSURLSessionTaskStateCompleted) { [task cancel]; } }]; }]; RACSignal *title = [fetchData flattenMap:^RACSignal *(NSDictionary *value) { if ([value[@"title"] isKindOfClass:[NSString class]]) { return [RACSignal return:value[@"title"]]; } else { return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]]; } }]; RACSignal *desc = [fetchData flattenMap:^RACSignal *(NSDictionary *value) { if ([value[@"desc"] isKindOfClass:[NSString class]]) { return [RACSignal return:value[@"desc"]]; } else { return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]]; } }]; RACSignal *renderedDesc = [desc flattenMap:^RACStream *(NSString *value) { NSError *error = nil; RenderManager *renderManager = [[RenderManager alloc] init]; NSAttributedString *rendered = [renderManager renderText:value error:&error]; if (error) { return [RACSignal error:error]; } else { return [RACSignal return:rendered]; } }]; RAC(self.someLablel,text) = [[title catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."]; RAC(self.originTextView,text) = [[desc catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."]; RAC(self.renderedTextView,attributedText) = [[renderedDesc catchTo:[RACSignal return:[[NSAttributedString alloc] initWithString:@"Error"]]] startWith:[[NSAttributedString alloc] initWithString:@"Loading..."]]; [[RACSignal merge:@[title,desc,renderedDesc]] subscribeError:^(NSError *error) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:error.domain delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; }]; 不知道大家有没有被这么一大段的代码吓到,我想要表达的是,在真正的工程中,我们的业务逻辑是很复杂的,而一些坑就隐藏在如此看似复杂但是又很合理的代码之下。所以我尽量模拟了一些需求,使得代码看起来更丰富。下面我们还是来仔细看下这段代码的逻辑吧:
这些代码体现了RAC的一些优势,例如良好的错误处理和各种链式处理。很不错,对不对?但是很遗憾的告诉大家,这段代码其实有很严重的错误。 如果你去尝试运行这段代码,并且打开Charles查看,你会惊奇的发现,这个网络请求发送了6次。没错,是6次请求。我们也可以想象到类似的代码存在其他副作用的问题,重新刷新了6次屏幕,写入6次文件,发了6个全局通知。 下面来分析,为什么是6次网络请求呢?首先根据上面的知识,可以推断出名为 这里有一个很重要的概念,就是任何的信号转换即是对原有的信号进行订阅从而产生新的信号。由此我们可以写出flattenMap的伪代码如下: - (instancetype)flattenMap_:(RACStream * (^)(id value))block { { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { return [self subscribeNext:^(id x) { RACSignal *signal = (RACSignal *)block(x); [signal subscribeNext:^(id x) { [subscriber sendNext:x]; } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ [subscriber sendCompleted]; }]; } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ [subscriber sendCompleted]; }]; }]; } 除了没有高度复用和缺少一些disposable的处理以外,上述代码大致可以比较直观地说明flattenMap的机制。观察会发现其实是在调用这个方法的时候,生成了一个新的信号,并在这个新信号的执行过程中对 回到我们的问题,那就是说,在 另外的3次订阅来自 由此可以看到,不熟悉冷热信号对业务造成的影响。我们可以想象对用户流量的影响,对服务器负载的影响,对统计的影响,如果这是一个点赞的接口,会不会造成多次点赞?后果不堪设想啊。而这些都可以通过将 接下来也许你会问,如果我的整个计算过程中都没有副作用,是否就不会有这个问题?答案是肯定的。试想下刚才那段代码如果没有网络请求,换成一些标准化的计算会怎样。虽然可以肯定它不会出现bug,但是不要忽视其中的运算也会执行多次。纯函数还有一个概念就是引用透明)。在纯函数式语言(例如Haskell)中对此可以进行一定的优化,也就是说纯函数的调用在相同参数下的返回值第二次不需要计算,所以在纯函数式语言里面的FRP并没有冷信号的担忧。然而Objective-C语言中并没有这种纯函数优化,因此有大规模运算的冷信号对性能是有一定影响的。 从上文内容可以看出,如果我们想更好地掌握RAC这个框架,区分冷信号与热信号是十分重要的。接下来的系列第三篇文章,我会揭示冷信号与热信号的本质,帮助大家正确的理解冷信号与热信号。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |