ReactiveCocoa 2

ReactiveCocoaGithub开源的一款cocoa FRP 框架,我在 之前的文章里有过介绍(当时还是1.x版本,2.x版本有了新的变化,API也有部分不兼容) 这里再简单地提一下。
Native app有很大一部分的时间是在等待事件发生,然后响应事件,比如等待网络请求完成,等待用户的操作,等待某些状态值的改变等等,等这些事件发生后,再做进一步处理。 但是这些等待和响应,并没有一个统一的处理方式。Delegate,Notification,Block,KVO,常常会不知道该用哪个最合适。有时需要chain或者compose某几个事件,就需要多个状态变量,而状态变量一多,复杂度也就上来了。为了解决这些问题,Github的工程师们开发了ReactiveCocoa。
几个常见的概念
在阅读ReactiveCocoa(以下简称RAC)的相关文章或代码时,经常会出现一些名词,理解它们对于理解RAC有很大的帮助,下面就简要来说说这些常见的概念。
Signal and Subscriber
这是RAC最核心的内容,这里我想用插头和插座来描述,插座是Signal,插头是Subscriber。想象某个遥远的星球,他们的电像某种物质一样被集中存储,且很珍贵。插座负责去获取电,插头负责使用电,而且一个插座可以插任意数量的插头。当一个插座(Signal)没有插头(Subscriber)时什么也不干,也就是处于冷(Cold)的状态,只有插了插头时才会去获取,这个时候就处于热(Hot)的状态。
Signal获取到数据后,会调用Subscriber的sendNext,sendComplete,sendError方法来传送数据给Subscriber,Subscriber自然也有方法来获取传过来的数据,如:[signal subscribeNext:error:completed]。这样只要没有sendComplete和sendError,新的值就会通过sendNext源源不断地传送过来,举个简单的例子:
 
 
  1. [RACObserve(self,username)subscribeNext:^(NSString*newName){
  2. NSLog(@"newName:%@",newName);
  3. }];
RACObserve使用了KVO来监听property的变化,只要username被自己或外部改变,block就会被执行。但不是所有的property都可以被RACObserve,该property必须支持KVO,比如NSURLCache的currentDiskUsage就不能被RACObserve。
Signal是很灵活的,它可以被修改(map),过滤(filter),叠加(combine),串联(chain),这有助于应对更加复杂的情况,比如:
  
  
  • RAC(self.logInButton,enabled)=[RACSignal
  • combineLatest:@[
  • self.usernameTextField.rac_textSignal,
  • self.passwordTextField.rac_textSignal,
  • RACObserve(LoginManager.sharedManager,loggingIn),
  • RACObserve(self,loggedIn)
  • ]reduce:^(NSString*username,NSString*password,NSNumber*loggingIn,NSNumber*loggedIn){
  • return@(username.length>0&&password.length>0&&!loggingIn.boolValue&&!loggedIn.boolValue);
  • }];
  • 这段代码看起来有点复杂,来细细说一下,首先是左边的RAC(...),它的作用是将self.logInButton.enabled属性与右边的signal的sendNext值绑定。也就是如果右边的reduce的返回值为NO,那么enabled就为NO。右边的combineLatest是获取这4个signal的next值。其中可以看到self.usernameTextField.rac_textSignal这么个小编,rac_textSignal是RAC为UITextField添加的category,只要usernameTextField的值有变化,这个值就会被返回(sendNext)。combineLatest需要每个signal至少都有过一次sendNext。reduce的作用是根据接收到的值,再返回一个新的值,这里是@(YES)和@(NO),必须是object。
    上面这段代码用到了Signal的组合,想象一下,如果是传统的方式,写起来还是挺复杂的,而且随着功能的增加,调整起来会更加麻烦。
    冷信号(Cold)和热信号(Hot)
    上面提到过这两个概念,冷信号默认什么也不干,比如下面这段代码
      
      
  • RACSignal*signal=[RACSignalcreateSignal:^RACDisposable*(id<RACSubscriber>subscriber){
  • NSLog(@"triggered");
  • [subscribersendNext:@"foobar"];
  • [subscribersendCompleted];
  • returnnil;
  • }];
  • 我们创建了一个Signal,但因为没有被subscribe,所以什么也不会发生。加了下面这段代码后,signal就处于Hot的状态了,block里的代码就会被执行。
      
      
  • [signalsubscribeCompleted:^{
  • NSLog(@"subscription%u",subscriptions);
  • }];
  • 或许你会问,那如果这时又有一个新的subscriber了,signal的block还会被执行吗?这就牵扯到了另一个概念:Side Effect
    Side Effect
    还是上面那段代码,如果有多个subscriber,那么signal就会又一次被触发,控制台里会输出两次triggered。这或许是你想要的,或许不是。如果要避免这种情况的发生,可以使用 replay 方法,它的作用是保证signal只被触发一次,然后把sendNext的value存起来,下次再有新的subscriber时,直接发送缓存的数据。
    Cocoa Categories
    为了更加方便地使用RAC,RAC给Cocoa添加了很多category,与系统集成地越紧密,使用起来自然也就越方便。下面是我认为比较常用的categories。
    UIView Categories
    上面看到的rac_textSignal是加在UITextField上的(UITextField+RACSignalSupport.h),其他常用的UIView也都有添加相应的category,比如UIAlertView,就不需要再用Delegate了。
      
      
  • UIAlertView*alertView=[[UIAlertViewalloc]initWithTitle:@""message:@"Alert"delegate:nilcancelButtonTitle:@"YES"otherButtonTitles:@"NO",nil];
  • [[alertViewrac_buttonClickedSignal]subscribeNext:^(NSNumber*indexNumber){
  • if([indexNumberintValue]==1){
  • NSLog(@"youtouchedNO");
  • }else{
  • NSLog(@"youtouchedYES");
  • }
  • }];
  • [alertViewshow];
  • 有了这些Category,大部分的Delegate都可以使用RAC来做。或许你会想,可不可以subscribe NSMutableArray.rac_sequence.signal,这样每次有新的object或旧的object被移除时都能知道,UITableViewController就可以根据dataSource的变化,来reloadData。但很可惜这样不行,因为RAC是基于KVO的,而NSMutableArray并不会在调用addObject或removeObject时发送通知,所以不可行。不过可以使用NSArray作为UITableView的dataSource,只要dataSource有变动就换成新的Array,这样就可以了。
    说到UITableView,再说一下UITableViewCell,RAC给UITableViewCell提供了一个方法:rac_prepareForReuseSignal,它的作用是当Cell即将要被重用时,告诉Cell。想象Cell上有多个button,Cell在初始化时给每个button都addTarget:action:forControlEvents,被重用时需要先移除这些target,下面这段代码就可以很方便地解决这个问题:
      
      
  • [[[self.cancelButton
  • rac_signalForControlEvents:UIControlEventTouchUpInside]
  • takeUntil:self.rac_prepareForReuseSignal]
  • subscribeNext:^(UIButton*x){
  • //dootherthings
  • }];
  • 还有一个很常用的category就是UIButton+RACCommandSupport.h,它提供了一个property:rac_command,就是当button被按下时会执行的一个命令,命令被执行完后可以返回一个signal,有了signal就有了灵活性。比如点击投票按钮,先判断一下有没有登录,如果有就发HTTP请求,没有就弹出登陆框,可以这么实现。
      
      
  • voteButton.rac_command=[[RACCommandalloc]initWithEnabled:self.viewModel.voteCommand.enabledsignalBlock:^RACSignal*(idinput){
  • //Assumethatwe'reloggedinatfirst.We'llreplacethissignallaterifnot.
  • RACSignal*authSignal=[RACSignalempty];
  • if([[PXRequestapiHelper]authMode]==PXAPIHelperModeNoAuth){
  • //Notloggedin.Replacesignal.
  • authSignal=[[RACSignalcreateSignal:^RACDisposable*(id<RACSubscriber>subscriber){
  • @strongify(self);
  • FRPLoginViewController*viewController=[[FRPLoginViewControlleralloc]initWithNibName:@"FRPLoginViewController"bundle:nil];
  • UINavigationController*navigationController=[[UINavigationControlleralloc]initWithRootViewController:viewController];
  • [selfpresentViewController:navigationControlleranimated:YEScompletion:^{
  • [subscribersendCompleted];
  • }];
  • returnnil;
  • }]];
  • }
  • return[authSignalthen:^RACSignal*{
  • @strongify(self);
  • return[[self.viewModel.voteCommandexecute:nil]ignoreValues];
  • }];
  • }];
  • [voteButton.rac_command.errorssubscribeNext:^(idx){
  • [xsubscribeNext:^(NSError*error){
  • [SVProgressHUDshowErrorWithStatus:[errorlocalizedDescription]];
  • }];
  • }];
  • 这段代码节选自AshFurrow的FunctionalReactivePixels,有删减。
    Data Structure Categories
    常用的数据结构,如NSArray,NSDictionary也都有添加相应的category,比如NSArray添加了rac_sequence,可以将NSArray转换为RACSequence,顺便说一下RACSequence,RACSequence是一组immutable且有序的values,不过这些values是运行时计算的,所以对性能提升有一定的帮助。RACSequence提供了一些方法,如array转换为NSArray,any:检查是否有Value符合要求,all:检查是不是所有的value都符合要求,这里的符合要求的,block返回YES,不符合要求的就返回NO。
    NotificationCenter Category
    NSNotificationCenter,默认情况下NSNotificationCenter使用Target-Action方式来处理Notification,这样就需要另外定义一个方法,这就涉及到编程领域的两大难题之一:起名字。有了RAC,就有Signal,有了Signal就可以subscribe,于是NotificationCenter就可以这么来处理,还不用担心移除observer的问题。
      
      
  • [[[NSNotificationCenterdefaultCenter]rac_addObserverForName:@"MyNotification"object:nil]subscribeNext:^(NSNotification*notification){
  • NSLog(@"NotificationReceived");
  • }];
  • NSObject Categories
    NSObject有不少的Category,我觉得比较有用的有这么几个
    NSObject+RACDeallocating.h
    顾名思义就是在一个object的dealloc被触发时,执行的一段代码。
      
      
  • NSArray*array=@[@"foo"];
  • [[arrayrac_willDeallocSignal]subscribeCompleted:^{
  • NSLog(@"oops,iwillbegone");
  • }];
  • array=nil;
  • NSObject+RACLifting.h
    有时我们希望满足一定条件时,自动触发某个方法,有了这个category就可以这么办
      
      
  • -(void)test
  • {
  • RACSignal*signalA=[RACSignalcreateSignal:^RACDisposable*(id<RACSubscriber>subscriber){
  • doubledelayInSeconds=2.0;
  • dispatch_time_tpopTime=dispatch_time(DISPATCH_TIME_NOW,(int64_t)(delayInSeconds*NSEC_PER_SEC));
  • dispatch_after(popTime,dispatch_get_main_queue(),^(void){
  • [subscribersendNext:@"A"];
  • });
  • returnnil;
  • }];
  • RACSignal*signalB=[RACSignalcreateSignal:^RACDisposable*(id<RACSubscriber>subscriber){
  • [subscribersendNext:@"B"];
  • [subscribersendNext:@"AnotherB"];
  • [subscribersendCompleted];
  • returnnil;
  • }];
  • [selfrac_liftSelector:@selector(doA:withB:)withSignals:signalA,signalB,nil];
  • }
  • -(void)doA:(NSString*)AwithB:(NSString*)B
  • {
  • NSLog(@"A:%@andB:%@",A,B);
  • }
  • 这里的rac_liftSelector:withSignals 就是干这件事的,它的意思是当signalA和signalB都至少sendNext过一次,接下来只要其中任意一个signal有了新的内容,doA:withB这个方法就会自动被触发。
    如果你有兴趣,可以想想上面这段代码会输出什么。
    NSObject+RACSelectorSignal.h
    这个category有rac_signalForSelector:和rac_signalForSelector:fromProtocol: 这两个方法。先来看前一个,它的意思是当某个selector被调用时,再执行一段指定的代码,相当于hook。比如点击某个按钮后,记个日志。后者表示该selector实现了某个协议,所以可以用它来实现Delegate。
    MVVM
    RAC带来的变化还不仅仅是这些,它还带来了架构层面的变化。我们都知道苹果推荐的是MVC架构,那MVVM又是什么呢?
    跟MVC最大的区别是多了个ViewModel,它直接与View绑定,而且对View一无所知。拿做菜打比方的话,ViewModel就是调料,它不关心做的到底是什么菜。这不是跟Model很像吗?是的,它可以扮演Model的职责,但其实它是Model的中介,这样当Model的API有变化,或者由本地存储变为远程API调用时,ViewModel的public API可以保持不变。
    使用ViewModel的好处是,可以让Controller更加简单和轻便,而且ViewModel相对独立,也更加方便测试和重用。那Controller这时又该做哪些事呢?在MVVM体系中,Controller可以被看成View,所以它的主要工作是处理布局、动画、接收系统事件、展示UI。
    MVVM还有一个很重要的概念是 data binding,view的呈现需要data,这个data就是由ViewModel提供的,将view的data与ViewModel的data绑定后,将来双方的数据只要一方有变化,另一方就能收到。这里有Github 开源的一个ViewModel Base Class。
    其他
    RAC在使用时有一些注意事项,可以参考官方的 DesignGuildLines,这里简单说一下。
    当一个signal被一个subscriber subscribe后,这个subscriber何时会被移除?答案是当subscriber被sendComplete或sendError时,或者手动调用[disposable dispose]。
    当subscriber被dispose后,所有该subscriber相关的工作都会被停止或取消,如http请求,资源也会被释放。
    Signal events是线性的,不会出现并发的情况,除非显示地指定Scheduler。所以-subscribeNext:error:completed:里的block不需要锁定或者synchronized等操作,其他的events会依次排队,直到block处理完成。
    Errors有优先权,如果有多个signals被同时监听,只要其中一个signal sendError,那么error就会立刻被传送给subscriber,并导致signals终止执行。相当于Exception。
    生成Signal时,最好指定Name,-setNameWithFormat: 方便调试。
    block代码中不要阻塞。
    小结
    尽管洋洋洒洒写了这么多,也只是对RAC有了个大概的了解,如果要更深入地了解RAC还是需要多读文档、代码和相关项目。
    RAC学习起来稍显吃力,且相关的文章目前还不多,中文的就更少了,希望这篇文章能带给你些帮助。
    以下是我觉得还不错的RAC相关资源
    FunctionalReactivePixels作者同时还出了一本FRP相关的书,个人觉得看源码就足够了。
    GroceryList RAC的作者之一 jspahrsummers 的一个项目
    ReactiveCocoa Essentilas: Understanding and Using RACCommand介绍了RACCommand的使用,同时也涉及了RAC相关的一些点。
    Transparent OAuth Token Refresh Using ReactiveCocoa这篇文章讲了如何使用RAC来透明地获取Access Token,然后继续发送请求。

    版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

    相关推荐


    react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
    我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
    我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
    react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
    mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
    我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
    本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
    上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
    上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
    我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
    在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
    楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
    我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
    前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
    我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
    上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
    开源不易,感谢你的支持,❤ star me if you like concent ^_^
    hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
    本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
    ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom