Reactive Cocoa Tutorial = 只取所需的Filters

2 篇文章 0 订阅

原文链接:http://www.cocoachina.com/industry/20140630/8985.html


概览

 

简而言之,Reactive Cocoa(RAC)就是一个函数响应式编程思想在Cocoa下的实现。
 
说说在RAC框架下做了一个项目的赶脚吧:
 
挺新鲜挺有意思,开发人员水平很高,框架封装性和实用性一流,看了看人家对宏的使用发现原来用的纯小儿科,对编译器的控制,block的使用也很值得的学习。
 
编程思想上的一些改变。原创的一个可能也不大恰当的比喻:原来的编程思想像是“走迷宫”,RAC的编程思想是“建迷宫”。意思是,之前的编程思路是命令式,大概都是“程序启动时执行xxxx,在用户点击后的回调函数执行xxx,收到一个Notification后执行xxx”等等,如同走迷宫一样,走出迷宫需要在不同时间段记住不同状态根据不同情况而做出一系列反应,继而走出迷宫;相比下,RAC的思想是建立联系,像钟表中的齿轮组一样,一个扣着一个,从转动发条到指针走动,一个齿轮一个齿轮的传导(Reactive),复杂但完整而自然。如同迷宫的建造者,在设计时早已决定了哪里是通路,哪里是死路或是哪个路口指向了出口,当一个挑战者(Event)走入迷宫时(Signal),他一定会在设置好的迷宫中的某个路线行走(传递),继而走到终点(Completion)或困死在里面(Error)。
 
写出代码结构明显不一样。由于RAC将Cocoa中KVO、UIKit Event、delegate、selector等都增加了RAC支持,所以都不用去做很多跨函数的事,比如KVO个对象然后在回调里面xxx,从storyboard里面连个UIButton的IBAction出来xxx,或是设个UITextField的delegate出来去取输入的文本xxx。但在RAC下就像上面比喻的建迷宫,把这些大都放在“-viewDidLoad:”就可以了,当然像UITableView的delegate和data source这么大规模的代理模式就还是老老实实写吧。
 
举个例子:
 
我就想干这么个事:“一个label一个text field,下面输啥上面显示啥”
 
老写法大概做法是这个vc实现UITextFieldDelegate协议,把这个text field的delegate设到vc上面,然后在要改变text的那个delegate方法里面取当前text field的text值,再赋给label上;
 
使用RAC的话就一句话(当然得把这俩控件都IBOutlet出来):
 
 
  1. RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; 
看着就挺爽。复杂的例子先不举了。
 
总之吧,等今后维护RAC的开发者和使用者把更多的Cocoa的东西归入RAC的框架中,这个框架基本上都可以凌驾于Cocoa这个框架了,意思是甚至用不着知道那些delegate啊KVO啊苹果告诉你是咋用的,用RAC封装的就行了。RAC对于值的显示大都是和property“绑定”的关系,像使用storyboard构建页面时,对于有响应的控件基本都得IBOutlet出来作为一个property,而不是像原来一样连个IBAction出来或者连个delegate出来。对于视图层到model层之间的绑定就显得有些生硬了,相当于视图直接耦合了model,于是应运而生M-V-VM结构,说白了就是在View和Model之间增加了一个ViewModel来解耦,这样View里面要做的基本就是绑定VM以及一些纯视图的操作(比如用什么动画效果展示一个数据);VM里面是和View相关的数据部分的储存和操作,比如说一个UITableView的data source,一个对email输入合法性的验证方法,当然还有的是对真正Model层的调用和结果的刷新,由于View已经和VM绑定,这样VM在刷新的时候只刷新自己的属性就得了。
 
其实重要的还是写代码思维方式的变化,如果全工程都使用RAC来实现,对于同一个业务逻辑终于可以在同一块代码里完成了,将UI事件,逻辑处理,文件或数据库操作,异步网络请求,UI结果显示,这一大套统统用函数式编程的思路嵌套起来,进入页面时搭建好这所有的关系,用户点击后妥妥的等着这一套联系一个个的按期望的逻辑和次序触发,最后显示给用户。感觉就像是搭好了一个精致的游乐场,然后不紧不慢地打开大门:@”Come on 熊孩子们!”
 
PS:写这个blog的时候用的RAC版本是2.2.3,现在有更高的版本,所以还会有很多变化。
 
神奇的Macros
 
先说说RAC中必须要知道的宏:
 
 
  1. RAC(TARGET, [KEYPATH, [NIL_VALUE]]) 
使用:
 
 
  1. RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; 
  2.  
  3. RAC(self.outputLabel, text, @"收到nil时就显示我") = self.inputTextField.rac_textSignal; 
这个宏是最常用的,RAC()总是出现在等号左边,等号右边是一个RACSignal,表示的意义是将一个对象的一个属性和一个signal绑定,signal每产生一个value(id类型),都会自动执行:
 
 
  1. [TARGET setValue:value ?: NIL_VALUE forKeyPath:KEYPATH]; 
数字值会升级为NSNumber *,当setValue:forKeyPath时会自动降级成基本类型(int, float ,BOOL等),所以RAC绑定一个基本类型的值是没有问题的
 
 
  1. · RACObserve(TARGET, KEYPATH) 
作用是观察TARGET的KEYPATH属性,相当于KVO,产生一个RACSignal
 
最常用的使用,和RAC宏绑定属性:
 
 
  1. RAC(self.outputLabel, text) = RACObserve(self.model, name); 
上面的代码将label的输出和model的name属性绑定,实现联动,name但凡有变化都会使得label输出
 
 
  1. @weakify(Obj); 
  2. @strongify(Obj); 
这对宏在 RACEXTScope.h 中定义,RACFramework好像没有默认引入,需要单独import
 
他们的作用主要是在block内部管理对self的引用:
 
 
  1. @weakify(self); // 定义了一个__weak的self_weak_变量 
  2. [RACObserve(self, name) subscribeNext:^(NSString *name) { 
  3.     @strongify(self); // 局域定义了一个__strong的self指针指向self_weak 
  4.     self.outputLabel.text = name; 
  5. }]; 
这个宏为什么这么吊,前面加@,其实就是一个啥都没干的@autoreleasepool {}前面的那个@,为了显眼罢了。
 
这两个宏一定成对出现,先weak再strong
 
除了RAC中常用宏的使用,有一些宏的实现方法也很值得观摩。
 
举个高级点的栗子:
 
要干的一件事,计算一个可变参数列表的长度。
 
第一反应就是用参数列表的api,va_start va_arg va_end遍历一遍计算个和,但仔细想想,对于可变参数这个事,在编译前其实就已经确定了,代码里括号里有多少个参数一目了然。
 
RAC中Racmetamarcos.h中就有一系列宏来完成这件事,硬是在预处理之后就拿到了可变参数个数:
 
 
  1. #define metamacro_argcount(...) \ 
  2.     metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) 
这个宏由几个工具宏一层层展开,现在模拟一下展开过程:
 
假如我们要计算的如下:
 
 
  1. int count = metamacro_argcount(a, b, c); 
于是乎第一层展开后:
 
 
  1. int count = metamacro_at(20, a, b, c, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) 
再看metamacro_at的定义:
 
 
  1. #define metamacro_at(N, ...) metamacro_concat(metamacro_at, N)(__VA_ARGS__) 
  2. // 下面是metamacro_concat做的事(简写一层) 
  3. #define metamacro_concat_(A, B) A ## B 
于是乎第二层展开后:
 
 
  1. int count = metamacro_at20(a, b, c, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); 
再看metamacro_at20这个宏干的事儿:
 
 
  1. #define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__) 
于是乎第三层展开后,相当于截断了前20个参数,留下剩下几个:
 
 
  1. int count = metamacro_head(3, 2, 1); 
这个metamacro_head:
 
 
  1. #define metamacro_head(...) metamacro_head_(__VA_ARGS__, 0) 
  2. #define metamacro_head_(FIRST, ...) FIRST 
后面加个0,然后取参数列表第一个,于是乎:
 
 
  1. int count = 3; 
大功告成。
 
反正我看完之后感觉挺震惊,宏还能这么用,这样带来的好处不止是将计算在预处理时搞定,不拖延到运行时恶心cpu;但更重要的是编译检查。比如某些可变参数的实现要求可以填2个参数,可以填3个参数,其他的都不行,这样,也只有这样的宏的实现,才能在编译前就确定了错误。
 
除了上面,还有一个神奇的宏的使用:
 
当使用诸如RAC(self, outputLabel)或RACObserve(self, name)时,发现写完逗号之后,输入第二个property的时候会出现完全正确的代码提示!这相当神奇。
探究一下,关键的关键是如下一个宏:
 
 
  1. #define keypath(...) \ 
  2.     metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__)) 
这个metamacro_argcount上面说过,是计算可变参数个数,所以metamacro_if_eq的作用就是判断参数个数,如果个数是1就执行后面的keypath1,若不是1就执行keypath2。
 
所以重点说一下keypath2:
 
 
  1. #define keypath2(OBJ, PATH) \ 
  2.     (((void)(NO && ((void)OBJ.PATH, NO)), # PATH)) 
乍一看真挺懵,先化简,由于Objc里面keypath是诸如”outputLabel.text”的字符串,所以这个宏的返回值应该是个字符串,可以简化成:
 
 
  1. #define keypath2(OBJ, PATH) (???????, # PATH) 
先不管”??????”是啥,这里不得不说C语言中一个不大常见的语法(第一个忽略):
 
 
  1. int a = 0, b = 0; 
  2. a = 1, b = 2; 
  3. int c = (a, b); 
这些都是逗号表达式的合理用法,第三个最不常用了,c将被b赋值,而a是一个未使用的值,编译器会给出warning。
 
去除warning的方法很简单,强转成void就行了:
 
 
  1. int c = ((void)a, b); 
再看上面简化的keypath2宏,返回的就是PATH的字符串字面值了(单#号会将传入值转成字面字符串)
 
 
  1. (((void)(NO && ((void)OBJ.PATH, NO)), # PATH)) 
对传入的第一个参数OBJ和第二个正要输入的PATH做了点操作,这也正是为什么输入第二个参数时编辑器会给出正确的代码提示。强转void就像上面说的去除了warning。
 
但至于为什么加入与NO做&&,我不太能理解,我测试时其实没有时已经完成了功能,可能是作者为了屏蔽某些隐藏的问题吧。
 
这个宏的巧妙的地方就在于使得编译器以为我们要输入“点”出来的属性,保证了输入值的合法性(输了不存在的property直接报错的),同时利用了逗号表达式取逗号最后值的语法返回了正确的keypath。
 
总之
RAC对宏的使用达到了很高的水平,还有诸如RACTuplePack,RACTupleUnpack的宏就不细说了,值得研究。
 
PS:上面介绍的metamacro和@strongify等宏确切来说来自RAC依赖的extobjc,作者是Justin Spahr-Summers,正是RAC作者之一。
 
百变RACStream
 
在RAC下开发干的最多的事就是建立RACSignal和subscribe RACSignal了,它是RAC的核心所在。本篇介绍了RAC的运作原理和设计思路,从函数式编程形成的RACStream继而介绍它的子类 - RAC最核心的部分RACSignal。
 
函数式编程
我们知道Reactive Cocoa是函数式编程(Functional Programing)(FP)思想的实现。FP有一套成熟的理论,这里只讲讲我个人理解吧。
 
我觉得FP就是“像计算函数表达式一样来解决一个问题”,举个栗子,中学题:
 
 
  1. 已知:f(x) = 2sin(x + π/2), 求 f(π/2)的值。 
其中x是这个函数的输入,f(x)为计算的输出结果,求f(π/2)时给定了x自然能计算出个结果来(说实话我真忘了咋算了)
 
当然,仔细看这个函数,其实是可以分解成几个小函数的:
 
 
  1. f1(x) = x + π/2 
  2. f2(x) = sin(x) 
  3. f3(x) = 2x 
而原来的f(x)可以被小函数组合:
 
 
  1. f(x) = f3(f2(f1(x))) 
所以不难得出这么个推论:要是我手上有足够的基本函数,我就能用上面的组合的方法组合出任意一个复杂的函数了。再想想事实上这些年来学数学的过程不就是在一个个积累基本函数的过程嘛,从基本运算,到三角函数,到乘方开方,再到微积分。基本函数越来越多,能解决的数学问题也越来越复杂。
 
再来看一个函数是怎么构成的,FP理论里叫monads,十分抽象,没读懂,但能理解出来:一个函数只要有一个对于输入值的运算方法和一个返回值,就够了。也容易理解,给它一个输入,干点事情,给出一个输出,就行了,当然现实情况要复杂得多(比如说输出值本身就是个函数?)有些函数是有输入的条件的,比如原来数学解个函数时候经常跟个作用域或者限制条件,比如f(x) = 10 / x , (x不为0),要是传个0这个函数就认为计算错误。
 
对于像上面栗子的函数,每个函数都能接收上一个函数输出的结果,作为自己的输入,这样才能嵌套生成最终结果,同时,计算的顺序也是一定从里向外,所以换个写法可以写成:
 
 
  1. start ---x--> f1(x) --(temp value1)--> f2(temp value1) --(temp value2)--> f3(temp value2) ---> result 
于是乎嵌套就被表示成了序列,来个高大上的名字怎么样,就叫流(Stream)
 
RACStream
这就是RACStream所表示的含义。
 
按照上面说的,其实RACStream的名字有点点歧义,对于一个RACStream对象,它在意义上等同于上面的f1(x),f2(x),f3(x),而不是那一大串整体,表示整体的应该是最外层的和f(x)对应的那个对象,叫个RACStreamComponent比较好?理解时候得注意下。
 
所以作为一个基本函数的RACStream应该至少应该有:
 
1、怎么传入值
2、怎么返回值
3、怎么与其他函数组合
4、怎么实现函数的作用域(监测输入值来做处理)
5、这函数叫啥- -
 
得益于在Objc下实现,所以输入输出的“值”都用个id类型就行了,遇到多个值的组合就用RACTurple(可以把多个值压包和解包,类比WINRAR),1和2解决
 
RACStream从实例变量来看只有一个name,当然它也只应该有个name - -,5解决
 
里面重点问题就是上面的3和4了。由于函数组合之后仍然是个函数,所以也很容易理解两个Stream对象的组合其实就是生成一个新的Stream对象,它返回了分别由两个子Stream先后运算产生的最终结果
 
观摩一下RACStream定义的基本方法:
 
 
  1. + (instancetype)empty; 
  2. + (instancetype)return:(id)value; 
  3. - (instancetype)bind:(RACStreamBindBlock (^)(void))block; // for 4 
  4. - (instancetype)concat:(RACStream *)stream; // for 3 
  5. - (instancetype)zipWith:(RACStream *)stream; // for 3 
RACStream作为一个描述抽象的父类,这几个基本方法并没有实现,是由具体子类来实现,RACStream的两个子类分别是RACSignal和RACSequence
 
+empty 是一个不返回值,立刻结束(Completed)的函数,意思是执行它之后除了立刻结束啥都不会发生,可以理解为RAC里面的nil。
+return: 是一个直接返回给定值,然后立刻结束的函数,比如 f(x) = 213
-bind:是一个非常重要的函数,在Rac Doc中被描述为‘basic primitives, particularly’,它是RACStream监测“值”和控制“运行状态”的基本方法,个人认为看注释文档不能理解它是干嘛的,而且bind英语“捆绑,绑定,强迫,约束”这几个意思也感觉对不上,我觉得叫“绑架”倒是更贴切一点。在-bind:之后,之前的RACStream就处于被“绑架”的状态,被绑架的RACStream每产生一个值,都要经过“绑架者”来决定:
 
1、是否使这个RACStream结束(被绑架者是否还能继续活着)
 
2、用什么新的RACStream来替换被绑架的RACStream,传出的结果也成了新RACStream产生的值(绑匪可以选择再抓一个人质放之前那个前面)
 
举个具体栗子,RACStream的 - take:方法,这个方法使一个RACStream只取前N次的值(有缩减):
 
 
  1. - (instancetype)take:(NSUInteger)count { 
  2.     Class class = self.class
  3.      
  4.     return [[self bind:^{ // self被绑架 
  5.         __block NSUInteger taken = 0; 
  6.  
  7.         return ^ id (id value, BOOL *stop) { // 这个block在被绑架的self每输出一个值得时候触发 
  8.             RACStream *result = class.empty; 
  9.  
  10.             if (taken < count) result = [class return:value]; // 未达到N次时将原值原原本本的传递出去 
  11.             if (++taken >= count) *stop = YES; // 达到第N次值后干掉了被绑架的self 
  12.  
  13.             return result; // 将被绑架的self替换为result 
  14.         }; 
  15.     }]]; 
-concat: 和 -zipWith: 就是将两个RACStream连接起来的基本方法了:
 
[A concat:B]中A和B像皇上和太子的关系,A是皇上,B是太子。皇上健在的时候统治天下发号施令(value),太子就候着,不发号施令(value),当皇上挂了(completed),太子登基当皇上,此时发出的号令(value)是太子的。
[C zipWith:D]可以比喻成一对平等恩爱的夫妻,两个人是“绑在一起“的关系来组成一个家庭,决定一件事(value)时必须两个人都提出意见(当且仅当C和D同时都产生了值的时候,一个value才被输出,CD只有其中一个有值时会挂起等待另一个的值,所以输出都是一对值(RACTuple)),当夫妻只要一个人先挂了(completed)这个家庭(组合起来的RACStream)就宣布解散(也就是无法凑成一对输出时就终止)
 
除了上面几个基本方法,RACStream还有不少的Operation方法,这些操作方法的实现大都是组合基本的方法来达到特定的目的,虽然是RACStream这个基类实现的,但我觉得还是放在后面介绍RACSignal的时候作为它的使用方法来说比较合适,毕竟绝大多数编程的对象的都是RACStream的两个子类,后面再展开介绍好了。
 
RACSignal的巧克力工厂
 
上面介绍了函数式编程和RACStream,使得函数得以串联起来,而它的具体子类,也是RAC编程中最重要的部分,RACSignal就是使得算式得以逐步运算并使其有意义的关键所在,本节主要介绍RACSignal的机理,具体的使用放到接下来的几节。
 
巧克力工厂的运作模式
RACStream实现了一个嵌套函数的结构,如f(x) = f1(f2(f3(x))),但好像是考试卷子上的一道题,没有人去做它,没得出个结果的话这道题是没有意义的。
 
OK,现在起将这个事儿都比喻成一个巧克力工厂,f(x)的结果是一块巧克力,f1,f2,f3代表巧克力生产的几个步骤,如果这个工厂不开工,它是没有意义的。
 
再说RACSignal,引用RAC doc的描述: “A signal, represented by the RACSignal class, is a push-driven stream.”
 
我觉得这个push-driven要想解释清楚,需要和RACSequence的pull-driven放在一起来看。在巧克力工厂,push-driven是“生产一个吃一个”,而pull-driven是“吃完一个才生产下一个”,对于工厂来说前者是主动模式:生产了巧克力就“push”给各个供销商,后者是被动模式:各个供销商过来“pull”产品时才给你现做巧克力。
 
Status
 
所以,对于RACSigna的push-driven的生产模式,首先,当工厂发现没有供销商签合同准备要巧克力的时候,工厂当然没有必要开动生产;只要当有一个以上准备收货的经销商时,工厂才开动生产。这就是RACSignal的休眠(cold)和激活(hot)状态,也就是所谓的冷信号和热信号。一般情况下,一个RACSignal创建之后都处于cold状态,有人去subscribe才被激活。
 
Event
RACSignal能产生且只能产生三种事件:next、completed,error。
 
next表示这个Signal产生了一个值(成功生产了一块巧克力)
 
completed表示Signal结束,结束信号只标志成功结束,不带值(一个批次的订单完成了)
 
error表示Signal中出现错误,立刻结束(一个机器坏了,生产线立刻停止运转)
 
工厂厂长存了所有供销商的QQ,每当发生上面三件事情的一件时,都用QQ挨个儿发消息告诉他们,于是供销商就能根据生产状态决定要做点什么。当订单完成或者失败后,厂长就会把这个供销商的QQ删了,以后发消息的时候也就没必要通知他了。
 
Side Effects
RACSignal在被subscribe的时候可能会产生副作用,先举个官方的栗子:
 
 
  1. __block int aNumber = 0; 
  2.  
  3. // Signal that will have the side effect of incrementing `aNumber` block 
  4. // variable for each subscription before sending it. 
  5. RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) { 
  6.     aNumber++; 
  7.     [subscriber sendNext:@(aNumber)]; 
  8.     [subscriber sendCompleted]; 
  9.     return nil; 
  10. }]; 
  11.  
  12. // This will print "subscriber one: 1" 
  13. [aSignal subscribeNext:^(id x) { 
  14.     NSLog(@"subscriber one: %@", x); 
  15. }]; 
  16.  
  17. // This will print "subscriber two: 2" 
  18. [aSignal subscribeNext:^(id x) { 
  19.     NSLog(@"subscriber two: %@", x); 
  20. }]; 
上面的signal在作用域外部引用了一个int变量,同时在signal的运算过程中作为next事件的值返回,这就造成了所谓的副作用,因为第二个订阅者的订阅而影响了输出值。
 
 
我的理解来看,这个事儿做的就不太地道,一个正经的函数式编程中的函数是不应该因为进行了运算而导致后面运算的值不统一的。但对于实际应用的情况来看也到无可厚非,比如用户点击了“登录”按钮,编程时把登录这个业务写为一个login的RACSignal,当然,第一次调用登录和再点一次第二次调用登录的结果肯定不一样了。所以说RAC式编程减少了大部分对临时状态值的定义,但不是全部哦。
 
怎么办呢?我觉得最好的办法就是“约定”,RAC design guide里面介绍了对于一个signal的命名法则:“Hot signals without side effects 最好使用property,如“textChanged”,不太理解什么情况用到这个,权当做一个静态的属性来看就行。
Cold signals without side effects 使用名词类型的方法名,如“-currentText”,“currentModels”,同时表明了返回值是个啥(这个尤其得注意,RACSignal的next值是id类型,所以全得是靠约定才知道具体返回类型)
Signals with side effects 这种就是像login一样有副作用的了,推荐使用动词类型的方法名,用对动词基本就能知道是不是有副作用了,比如“-loginSignal”和“-saveToFile”大概就知道前面一个很可能有副作用,后面一个多存几次文件应该没副作用。”
 
当然,也可以multicast一个event,使得某些特殊的情况来共享一个副作用,后面再具体讲,先一个官方的简单的栗子:
 
 
  1. // This signal starts a new request on each subscription. 
  2. RACSignal *networkRequest = [RACSignal createSignal:^(id<RACSubscriber> subscriber) { 
  3.     AFHTTPRequestOperation *operation = [client 
  4.         HTTPRequestOperationWithRequest:request 
  5.         success:^(AFHTTPRequestOperation *operation, id response) { 
  6.             [subscriber sendNext:response]; 
  7.             [subscriber sendCompleted]; 
  8.         } 
  9.         failure:^(AFHTTPRequestOperation *operation, NSError *error) { 
  10.             [subscriber sendError:error]; 
  11.         }]; 
  12.  
  13.     [client enqueueHTTPRequestOperation:operation]; 
  14.     return [RACDisposable disposableWithBlock:^{ 
  15.         [operation cancel]; 
  16.     }]; 
  17. }]; 
  18.  
  19. // Starts a single request, no matter how many subscriptions `connection.signal` 
  20. // gets. This is equivalent to the -replay operator, or similar to 
  21. // +startEagerlyWithScheduler:block:. 
  22. RACMulticastConnection *connection = [networkRequest multicast:[RACReplaySubject subject]]; 
  23. [connection connect]; 
  24.  
  25. [connection.signal subscribeNext:^(id response) { 
  26.     NSLog(@"subscriber one: %@", response); 
  27. }]; 
  28.  
  29. [connection.signal subscribeNext:^(id response) { 
  30.     NSLog(@"subscriber two: %@", response); 
  31. }]; 
当地一个订阅者subscribeNext的时候触发了AFNetworkingOperation的创建和执行,开始网络请求,此时又来了个订阅者订阅这个Signal,按理说这个网络请求会被“副作用”,重新发一遍,但做了上面的处理之后,这两个订阅者接收到了同样的一个请求的内容。
 
RACScheduler - 生产线
RACScheduler是RAC里面对线程的简单封装,事件可以在指定的scheduler上分发和执行,不特殊指定的话,事件的分发和执行都在一个默认的后台线程里面做,大多数情况也就不用动了,有一些特殊的signal必须在主线程调用,使用-deliverOn:可以切换调用的线程。
 
但值得特殊了解的事实是:However, RAC guarantees that no two signal events will ever arrive concurrently. While an event is being processed, no other events will be delivered. The senders of any other events will be forced to wait until the current event has been handled.
 
意思是订阅者执行时的block一定非并发执行,也就是说不会执行到一半被另一个线程进入,也意味着写subscribeXXX block的时候没必要做加锁处理了。
 
巧克力的生产工艺
RACSignal的厂子建好了,运行的模式也都想好了,剩下的就是巧克力的加工工艺了。
 
有了RACStream的嵌套和组装的基础,RACSignal得以使用组件化的工艺来一步步的加工巧克力,从可可,牛奶,糖等原料,混合到这种巧克力适用的液态巧克力,过滤,提纯,冷却,夹心,压模,再到包装,一个巧克力就产出了。对于不同种类的巧克力,比如酒心巧克力,也不过是把其中的某个组件替换成注入酒心罢了。
 
RACSignal的生产组件,也就是它的各式各样的operation,一个具体业务逻辑的实现,其实也就是选择合适operation按合适的顺序组合起来。
 
还举那个用户在textFiled输入并显示到上面的label中的栗子:
 
 
  1. RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; 
现在需求变成“用户输入3个字母以上才输出到label,当不足3个时显示提示”,OK,好办:
 
 
  1. RAC(self.outputLabel, text) = [[self.inputTextField.rac_textSignal 
  2.     startWith:@"key is >3"/* startWith 一开始返回的初始值 */ 
  3.     filter:^BOOL(NSString *value) { 
  4.         return value.length > 3; /* filter使满足条件的值才能传出 */ 
  5. }]; 
需求又增加成“当输入sunny时显示输入正确”
 
 
  1. RAC(self.outputLabel, text) = [[self.inputTextField.rac_textSignal 
  2.     startWith:@"key is >3"// startWith 一开始返回的初始值 
  3.     filter:^BOOL(NSString *value) { // filter使满足条件的值才能传出 
  4.         return value.length > 3; 
  5.     }] 
  6.     map:(NSString *value) { // map将一个值转化为另一个值输出 
  7.         return [value isEqualToString:@"sunny"] ? @"bingo!" : value; 
  8.     }]; 
可以看出,基本上一个业务逻辑经过分析后可以拆解成一个个小RACSignal的组合,也就像生产巧克力的一道道工艺了。上面的栗子慢慢感觉就像了一个简陋的输答案的框了。
 
只取所需的Filters
 
RAC中的Filters
 
画个范围
一个Signal源可以产生一系列next值,但并非所有值都是需要的,具体的Subscriber可以选择在原有Signal上套用Filter操作来过滤掉不需要的值。
我的定义:RAC中如果一个Operation将处理后的值集合是处理前值集合的子集,我们就可以把它归为Filter类型。
 
当然通过之前介绍的基础操作完全可以自己拼出个想要的filter来,RAC为了方便使用已经实现了几个常用的filter,经过总结,这些filter大概可以分成两类:next值过滤类型和起止点过滤类型
 
值过滤类型Filters
 
- filter: (BOOL (^)(id value))
RAC中的filter同名方法- filter:(BOOL (^)(id value)),简单明了,将一个value用block做test,返回YES的才会通过,它的内部实现使用了- flattenMap:,将原来的Signal经过过滤转化成只返回过滤值的Signal,用法也不难理解:
 
 
  1. [[self.inputTextField.rac_textSignal filter:^BOOL(NSString *value) { 
  2.     return [value hasPrefix:@"sunny"]; 
  3. }] subscribeNext:^(NSString *value) { 
  4.     NSLog(@"This value has prefix `sunny` : %@", value); 
  5. }]; 
此外,还有几个这个方法的衍生方法:
 
- ignore: (id)
忽略给定的值,注意,这里忽略的既可以是地址相同的对象,也可以是- isEqual:结果相同的值,也就是说自己写的Model对象可以通过重写- isEqual:方法来使- ignore:生效。常用的值的判断没有问题,如下:
 
 
  1. [[self.inputTextField.rac_textSignal ignore:@"sunny"] subscribeNext:^(NSString *value) { 
  2.     NSLog(@"`sunny` could never appear : %@", value); 
  3. }]; 
 
- ignoreValues
这个比较极端,忽略所有值,只关心Signal结束,也就是只取Comletion和Error两个消息,中间所有值都丢弃。
注意,这个操作应该出现在Signal有终止条件的的情况下,如rac_textSignal这样除dealloc外没有终止条件的Signal上就不太可能用到。
 
- distinctUntilChanged
也是一个相当常用的Filter(但它不是- filter:的衍生方法),它将这一次的值与上一次做比较,当相同时(也包括- isEqual:)被忽略掉。
比如UI上一个Label绑定了一个值,根据值更新显示的内容:
 
 
  1. RAC(self.label, text) = [RACObserve(self.user, username) distinctUntilChanged]; 
  2. self.user.username = @"sunnyxx"// 1st 
  3. self.user.username = @"sunnyxx"// 2nd 
  4. self.user.username = @"sunnyxx"// 3rd 
如果不增加distinctUntilChanged的话对于连续的相同的输入值就会有不必要的处理,这个栗子只是简单的UI刷新,但遇到如写数据库,发网络请求的情况时,代价就不能购忽略了。
 
所以,对于相同值可以忽略的情况,果断加上它吧。
 
起止点过滤类型
 
除了被动的当next值来的时候做判断,也可以主动的提前选择开始和结束条件,分为两种类型:
take型(取)和skip型(跳)
 
- take: (NSUInteger)
从开始一共取N次的next值,不包括Competion和Error,如:
 
 
  1. [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
  2.     [subscriber sendNext:@"1"]; 
  3.     [subscriber sendNext:@"2"]; 
  4.     [subscriber sendNext:@"3"]; 
  5.     [subscriber sendCompleted]; 
  6.     return nil; 
  7. }] take:2] subscribeNext:^(id x) { 
  8.     NSLog(@"only 1 and 2 will be print: %@", x); 
  9. }]; 
 
- takeLast: (NSUInteger)
取最后N次的next值,注意,由于一开始不能知道这个Signal将有多少个next值,所以RAC实现它的方法是将所有next值都存起来,然后原Signal完成时再将后N个依次发送给接收者,但Error发生时依然是立刻发送的。
 
- takeUntil:(RACSignal *)
当给定的signal完成前一直取值。最简单的栗子就是UITextField的rac_textSignal的实现(删减版本):
 
 
  1. - (RACSignal *)rac_textSignal { 
  2.     @weakify(self); 
  3.     return [[[[[RACSignal 
  4.         concat:[self rac_signalForControlEvents:UIControlEventEditingChanged]] 
  5.         map:^(UITextField *x) { 
  6.             return x.text; 
  7.         }] 
  8.         takeUntil:self.rac_willDeallocSignal] // bingo! 
也就是这个Signal一直到textField执行dealloc时才停止
 
- takeUntilBlock:(BOOL (^)(id x))
对于每个next值,运行block,当block返回YES时停止取值,如:
 
 
  1. [[self.inputTextField.rac_textSignal takeUntilBlock:^BOOL(NSString *value) { 
  2.     return [value isEqualToString:@"stop"]; 
  3. }] subscribeNext:^(NSString *value) { 
  4.     NSLog(@"current value is not `stop`: %@", value); 
  5. }]; 
 
- takeWhileBlock:(BOOL (^)(id x))
上面的反向逻辑,对于每个next值,block返回 YES时才取值
 
- skip:(NSUInteger)
从开始跳过N次的next值,简单的栗子:
 
 
  1. [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
  2.     [subscriber sendNext:@"1"]; 
  3.     [subscriber sendNext:@"2"]; 
  4.     [subscriber sendNext:@"3"]; 
  5.     [subscriber sendCompleted]; 
  6.     return nil; 
  7. }] skip:1] subscribeNext:^(id x) { 
  8.     NSLog(@"only 2 and 3 will be print: %@", x); 
  9. }]; 
 
- skipUntilBlock:(BOOL (^)(id x))
和- takeUntilBlock:同理,一直跳,直到block为YES
 
- skipWhileBlock:(BOOL (^)(id x))
和- takeWhileBlock:同理,一直跳,直到block为NO
 
总结
 
本章介绍了RAC中Filter类型的Operation,总结一下:
 
适用场景:需要一个next值集合的子集时
Filter类型:值过滤型和起止点过滤型
值过滤型常用方法: -filter:,-ignore:,-distinctUnitlChanged
起止点过滤型常用方法:take系列和skip系列
 
References
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值