ReactiveCocoa2实战


转自无网不剩的博客

之前已经写过两篇关于ReactiveCocoa(以下简称RAC)的文章了,但主要也是在阐述基本的概念和使用,这篇文章将会从实战的角度来看看RAC到底解决了哪些问题,带来了哪些方便,以及遇到的一些坑。
概述
为什么要使用RAC?
一个怪怪的东西,从Demo看也没有让代码变得更好、更短,相反还造成理解上的困难,真的有必要去学它么?相信这是大多数人在接触RAC时的想法。RAC不是单一功能的模块,它是一个Framework,提供了一整套解决方案。其核心思想是「响应数据的变化」,在这个基础上有了Signal的概念,进而可以帮助减少状态变量(可以参考jspahrsummers的 PPT),使用MVVM架构,统一的异步编程模型等等。
为什么RAC更加适合编写Cocoa App?说这个之前,我们先来看下Web前端编程,因为有些相似之处。目前很火的AngularJS有一个很重要的特性:数据与视图绑定。就是当数据变化时,视图不需要额外的处理,便可正确地呈现最新的数据。而这也是RAC的亮点之一。RAC与Cocoa的编程模式,有点像AngularJS和jQuery。所以要了解RAC,需要先在观念上做调整。
以下面这个Cell为例
正常的写法可能是这样,很直观。
     
     
  1. -(void)configureWithItem:(HBItem*)item
  2. {
  3. self.username.text=item.text;
  4. [self.avatarImageViewsetImageWithURL:item.avatarURL];
  5. //其他的一些设置
  6. }
但如果用RAC,可能就是这样
      
      
  • -(id)init
  • {
  • if(self=[superinit]){
  • @weakify(self);
  • [RACObserve(self,viewModel)subscribeNext:^(HBItemViewModel*viewModel){
  • @strongify(self);
  • self.username.text=viewModel.item.text;
  • [self.avatarImageViewsetImageWithURL:viewModel.item.avatarURL];
  • //其他的一些设置
  • }];
  • }
  • }
  • 也就是先把数据绑定,接下来只要数据有变化,就会自动响应变化。在这里,每次viewModel改变时,内容就会自动变成该viewModel的内容。
    Signal
    Signal是RAC的核心,为了帮助理解,画了这张简化图
    这里的数据源和sendXXX,可以理解为函数的参数和返回值。当Signal处理完数据后,可以向下一个Signal或Subscriber传送数据。可以看到上半部分的两个Signal是冷的(cold),相当于实现了某个函数,但该函数没有被调用。同时也说明了Signal可以被组合使用,比如RACSignal *signalB = [signalA map:^id(id x){return x}],或RACSignal *signalB = [signalA take:1]等等。
    当signal被subscribe时,就会处于热(hot)的状态,也就是该函数会被执行。比如上面的第二张图,首先signalA可能发了一个网络请求,拿到结果后,把数据通过sendNext方法传递到下一个signal,signalB可以根据需要做进一步处理,比如转换成相应的Model,转换完后再sendNext到subscriber,subscriber拿到数据后,再改变ViewModel,同时因为View已经绑定了ViewModel,所以拿到的数据会自动在View里呈现。
    还有,一个signal可以被多个subscriber订阅,这里怕显得太乱就没有画出来,但每次被新的subscriber订阅时,都会导致数据源的处理逻辑被触发一次,这很有可能导致意想不到的结果,需要注意一下。
    当数据从signal传送到subscriber时,还可以通过doXXX来做点事情,比如打印数据。
    通过这张图可以看到,这非常像中学时学的函数,比如 f(x) = y,某一个函数的输出又可以作为另一个函数的输入,比如 f(f(x)) = z,这也正是「函数响应式编程」(FRP)的核心。
    有些地方需要注意下,比如把signal作为local变量时,如果没有被subscribe,那么方法执行完后,该变量会被dealloc。但如果signal有被subscribe,那么subscriber会持有该signal,直到signal sendCompleted或sendError时,才会解除持有关系,signal才会被dealloc。
    RACCommand
    RACCommand是RAC很重要的组成部分,可以节省很多时间并且让你的App变得更Robust,这篇文章可以帮助你更深入的理解,这里简单做一下介绍。
    RACCommand 通常用来表示某个Action的执行,比如点击Button。它有几个比较重要的属性:executionSignals / errors / executing。
    1、executionSignals是signal of signals,如果直接subscribe的话会得到一个signal,而不是我们想要的value,所以一般会配合switchToLatest。
    2、errors。跟正常的signal不一样,RACCommand的错误不是通过sendError来实现的,而是通过errors属性传递出来的。
    3、executing表示该command当前是否正在执行。
    假设有这么个需求:当图片载入完后,分享按钮才可用。那么可以这样:
          
          
  • RACSignal*imageAvailableSignal=[RACObserve(self,imageView.image)map:id^(idx){returnx?@YES:@NO}];
  • self.shareButton.rac_command=[[RACCommandalloc]initWithEnabled:imageAvailableSignalsignalBlock:^RACSignal*(idinput){
  • //dosharelogic
  • }];
  • 除了与UIControl绑定之外,也可以手动执行某个command,比如双击图片点赞,就可以这么实现。
          
          
  • //ViewModel.m
  • -(instancetype)init
  • {
  • self=[superinit];
  • if(self){
  • void(^updatePinLikeStatus)()=^{
  • self.pin.likedCount=self.pin.hasLiked?self.pin.likedCount-1:self.pin.likedCount+1;
  • self.pin.hasLiked=!self.pin.hasLiked;
  • };
  • _likeCommand=[[RACCommandalloc]initWithSignalBlock:^RACSignal*(idinput){
  • //先展示效果,再发送请求
  • updatePinLikeStatus();
  • return[[HBAPIManagersharedManager]likePinWithPinID:self.pin.pinID];
  • }];
  • [_likeCommand.errorssubscribeNext:^(idx){
  • //发生错误时,回滚
  • updatePinLikeStatus();
  • }];
  • }
  • returnself;
  • }
  • //ViewController.m
  • -(void)viewDidLoad
  • {
  • [superviewDidLoad];
  • //...
  • @weakify(self);
  • [RACObserve(self,viewModel.hasLiked)subscribeNex:^(idx){
  • @strongify(self);
  • self.pinLikedCountLabel.text=self.viewModel.likedCount;
  • self.likePinImageView.image=[UIImageimageNamed:self.viewModel.hasLiked?@"pin_liked":@"pin_like"];
  • }];
  • UITapGestureRecognizer*tapGesture=[[UITapGestureRecognizeralloc]init];
  • tapGesture.numberOfTapsRequired=2;
  • [[tapGesturerac_gestureSignal]subscribeNext:^(idx){
  • [self.viewModel.likeCommandexecute:nil];
  • }];
  • }
  • 再比如某个App要通过Twitter登录,同时允许取消登录,就可以这么做 (source)
          
          
  • _twitterLoginCommand=[[RACCommandalloc]initWithSignalBlock:^(id_){
  • @strongify(self);
  • return[[self
  • twitterSignInSignal]
  • takeUntil:self.cancelCommand.executionSignals];
  • }];
  • RAC(self.authenticatedUser)=[self.twitterLoginCommand.executionSignalsswitchToLatest];
  • 常用的模式
    map + switchToLatest
    switchToLatest: 的作用是自动切换signal of signals到最后一个,比如之前的command.executionSignals就可以使用switchToLatest:。
    map:的作用很简单,对sendNext的value做一下处理,返回一个新的值。
    如果把这两个结合起来就有意思了,想象这么个场景,当用户在搜索框输入文字时,需要通过网络请求返回相应的hints,每当文字有变动时,需要取消上一次的请求,就可以使用这个配搭。这里用另一个Demo,简单演示一下
          
          
  • NSArray*pins=@[@172230988,@172230947,@172230899,@172230777,@172230707];
  • __blockNSIntegerindex=0;
  • RACSignal*signal=[[[[RACSignalinterval:0.1onScheduler:[RACSchedulerscheduler]]
  • take:pins.count]
  • map:^id(idvalue){
  • return[[[HBAPIManagersharedManager]fetchPinWithPinID:[pins[index++]intValue]]doNext:^(idx){
  • NSLog(@"这里只会执行一次");
  • }];
  • }]
  • switchToLatest];
  • [signalsubscribeNext:^(HBPin*pin){
  • NSLog(@"pinID:%d",pin.pinID);
  • }completed:^{
  • NSLog(@"completed");
  • }];
  • //output
  • //2014-06-0517:40:49.851这里只会执行一次
  • //2014-06-0517:40:49.851pinID:172230707
  • //2014-06-0517:40:49.851completed
  • takeUntil
    takeUntil:someSignal 的作用是当someSignal sendNext时,当前的signal就sendCompleted,someSignal就像一个拳击裁判,哨声响起就意味着比赛终止。
    它的常用场景之一是处理cell的button的点击事件,比如点击Cell的详情按钮,需要push一个VC,就可以这样:
          
          
  • [[[cell.detailButton
  • rac_signalForControlEvents:UIControlEventTouchUpInside]
  • takeUntil:cell.rac_prepareForReuseSignal]
  • subscribeNext:^(idx){
  • //generateandpushViewController
  • }];
  • 如果不加takeUntil:cell.rac_prepareForReuseSignal,那么每次Cell被重用时,该button都会被addTarget:selector。
    替换Delegate
    出现这种需求,通常是因为需要对Delegate的多个方法做统一的处理,这时就可以造一个signal出来,每次该Delegate的某些方法被触发时,该signal就会sendNext。
          
          
  • @implementationUISearchDisplayController(RAC)
  • -(RACSignal*)rac_isActiveSignal{
  • self.delegate=self;
  • RACSignal*signal=objc_getAssociatedObject(self,_cmd);
  • if(signal!=nil)returnsignal;
  • /*Createtwosignalsandmergethem*/
  • RACSignal*didBeginEditing=[[selfrac_signalForSelector:@selector(searchDisplayControllerDidBeginSearch:)
  • fromProtocol:@protocol(UISearchDisplayDelegate)]mapReplace:@YES];
  • RACSignal*didEndEditing=[[selfrac_signalForSelector:@selector(searchDisplayControllerDidEndSearch:)
  • fromProtocol:@protocol(UISearchDisplayDelegate)]mapReplace:@NO];
  • signal=[RACSignalmerge:@[didBeginEditing,didEndEditing]];
  • objc_setAssociatedObject(self,_cmd,signal,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  • returnsignal;
  • }
  • @end
  • 代码源于 此文
    使用ReactiveViewModel的didBecomActiveSignal
    ReactiveViewModel是另一个project, 后面的MVVM中会讲到,通常的做法是在VC里设置VM的active属性(RVMViewModel自带该属性),然后在VM里subscribeNext didBecomActiveSignal,比如当Active时,获取TableView的最新数据。
    RACSubject的使用场景
    一般不推荐使用RACSubject,因为它过于灵活,滥用的话容易导致复杂度的增加。但有一些场景用一下还是比较方便的,比如ViewModel的errors。
    ViewModel一般会有多个RACCommand,那这些commands如果出现error了该如何处理呢?比较方便的方法如下:
    //HBCViewModel.h
          
          
  • #import"RVMViewModel.h"
  • @classRACSubject;
  • @interfaceHBCViewModel:RVMViewModel
  • @property(nonatomic)RACSubject*errors;
  • @end
  • //HBCViewModel.m
  • #import"HBCViewModel.h"
  • #import<ReactiveCocoa.h>
  • @implementationHBCViewModel
  • -(instancetype)init
  • {
  • self=[if(self){
  • _errors=[RACSubjectsubject];
  • }
  • returnself;
  • }
  • -(void)dealloc
  • {
  • [_errorssendCompleted];
  • }
  • @end
  • //SomeOtherViewModelinheritHBCViewModel
  • -(instancetype)init
  • {
  • _fetchLatestCommand=[RACCommandalloc]initWithSignalBlock:^RACSignal*(idinput){
  • //fetchlatestdata
  • }];
  • _fetchMoreCommand=[RACCommandalloc]initWithSignalBlock:^RACSignal*(idinput){
  • //fetchmoredata
  • }];
  • [self.didBecomeActiveSignalsubscribeNext:^(idx){
  • [_fetchLatestCommandexecute:nil];
  • }];
  • [[RACSignal
  • merge:@[
  • _fetchMoreCommand.errors,
  • _fetchLatestCommand.errors
  • ]]subscribe:self.errors];
  • }
  • rac_signalForSelector
    rac_signalForSelector: 这个方法会返回一个signal,当selector执行完时,会sendNext,也就是当某个方法调用完后再额外做一些事情。用在category会比较方便,因为Category重写父类的方法时,不能再通过[super XXX]来调用父类的方法,当然也可以手写Swizzle来实现,不过有了rac_signalForSelector:就方便多了。
    rac_signalForSelector: fromProtocol: 可以直接实现对protocol的某个方法的实现(听着有点别扭呢),比如,我们想实现UIScrollViewDelegate的某些方法,可以这么写
          
          
  • [[selfrac_signalForSelector:@selector(scrollViewDidEndDecelerating:)fromProtocol:@protocol(UIScrollViewDelegate)]subscribeNext:^(RACTuple*tuple){
  • //dosomething
  • }];
  • [[selfrac_signalForSelector:@selector(scrollViewDidScroll:)fromProtocol:@protocol(UIScrollViewDelegate)]subscribeNext:^(RACTuple*tuple){
  • //dosomething
  • }];
  • self.scrollView.delegate=nil;
  • self.scrollView.delegate=self;
  • 注意,这里的delegate需要先设置为nil,再设置为self,而不能直接设置为self,如果self已经是该scrollView的Delegate的话。
    有时,我们想对selector的返回值做一些处理,但很遗憾RAC不支持,如果真的有需要的话,可以使用 Aspects
    MVVM
    这是一个大话题,如果有耐心,且英文还不错的话,可以看一下Cocoa Samurai的这 两篇文章。PS: Facebook Paper就是基于MVVM构建的。
    MVVM是Model-View-ViewModel的简称,它们之间的关系如下
    可以看到View(其实是ViewController)持有ViewModel,这样做的好处是ViewModel更加独立且可测试,ViewModel里不应包含任何View相关的元素,哪怕换了一个View也能正常工作。而且这样也能让View/ViewController「瘦」下来。
    ViewModel主要做的事情是作为View的数据源,所以通常会包含网络请求。
    或许你会疑惑,ViewController哪去了?在MVVM的世界里,ViewController已经成为了View的一部分。它的主要职责是将VM与View绑定、响应VM数据的变化、调用VM的某个方法、与其他的VC打交道。
    而RAC为MVVM带来很大的便利,比如RACCommand,UIKit的RAC Extension等等。使用MVVM不一定能减少代码量,但能降低代码的复杂度。
    以下面这个需求为例,要求大图滑动结束时,底部的缩略图滚动到对应的位置,并高亮该缩略图;同时底部的缩略图被选中时,大图也要变成该缩略图的大图。
    我的思路是横向滚动的大图是一个collectionView,该collectionView是当前页面VC的一个property。底部可以滑动的缩略图是一个childVC的collectionView,这两个collectionView共用一套VM,并且各自RACObserve感兴趣的property。
    比如大图滑到下一页时,会改变VM的indexPath属性,而底部的collectionView所在的VC正好对该indexPath感兴趣,只要indexPath变化就滚动到相应的Item
    //childVC
          
          
  • -(void)viewDidLoad
  • {
  • [superviewDidLoad];
  • @weakify(self);
  • [RACObserve(self,viewModel.indexPath)subscribeNext:^(NSNumber*index){
  • @strongify(self);
  • [selfscrollToIndexPath];
  • }];
  • }
  • -(void)scrollToIndexPath
  • {
  • if(self.collectionView.subviews.count){
  • NSIndexPath*indexPath=self.viewModel.indexPath;
  • [self.collectionViewscrollToItemAtIndexPath:indexPathatScrollPosition:UICollectionViewScrollPositionCenteredHorizontallyanimated:YES];
  • [self.collectionView.subviewsenumerateObjectsUsingBlock:^(UIView*view,NSUIntegeridx,BOOL*stop){
  • view.layer.borderWidth=0;
  • }];
  • UIView*view=[self.collectionViewcellForItemAtIndexPath:indexPath];
  • view.layer.borderWidth=kHBPinsNaviThumbnailPadding;
  • view.layer.borderColor=[UIColorwhiteColor].CGColor;
  • }
  • }
  • 当点击底部的缩略图时,上面的大图也要做出变化,也同样可以通过RACObserve indexPath来实现
    //PinsViewController.m
          
          
  • -(superviewDidLoad];
  • @weakify(self);
  • [[RACObserve(self,viewModel.indexPath)
  • skip:1]
  • subscribeNext:^(NSIndexPath*indexPath){
  • @strongify(self);
  • [self.collectionViewscrollToItemAtIndexPath:indexPathatScrollPosition:UICollectionViewScrollPositionCenteredHorizontallyanimated:YES];
  • }];
  • }
  • 这里有一个小技巧,当Cell里的元素比较复杂时,我们可以给Cell也准备一个ViewModel,这个CellViewModel可以由上一层的ViewModel提供,这样Cell如果需要相应的数据,直接跟CellViewModel要即可,CellViewModel也可以包含一些command,比如likeCommand。假如点击Cell时,要做一些处理,也很方便。
    //CellViewModel已经在ViewModel里准备好了
          
          
  • -(UICollectionViewCell*)collectionView:(UICollectionView*)collectionViewcellForItemAtIndexPath:(NSIndexPath*)indexPath
  • {
  • HBPinsCell*cell=[collectionViewdequeueReusableCellWithReuseIdentifier:cellIdentifierforIndexPath:indexPath];
  • cell.viewModel=self.viewModel.cellViewModels[indexPath.row];
  • returncell;
  • }
  • -(void)collectionView:(UICollectionView*)collectionViewdidSelectItemAtIndexPath:(NSIndexPath*)indexPath
  • {
  • HBCellViewModel*cellViewModel=self.viewModel.cellViewModels[indexPath.row];
  • //对cellViewModel执行某些操作,因为Cell已经与cellViewModel绑定,所以cellViewModel的改变也会反映到Cell上
  • //或拿到cellViewModel的数据来执行某些操作
  • }
  • ViewModel中signal,property,command的使用
    初次使用RAC+MVVM时,往往会疑惑,什么时候用signal,什么时候用property,什么时候用command?
    一般来说可以使用property的就直接使用,没必要再转换成signal,外部RACObserve即可。使用signal的场景一般是涉及到多个property或多个signal合并为一个signal。command往往与UIControl/网络请求挂钩。
    常见场景的处理
    检查本地缓存,如果失效则去请求网络数据并缓存到本地
          
          
  • -(RACSignal*)loadData{
  • return[[RACSignal
  • createSignal:^(id<RACSubscriber>subscriber){
  • //Ifthecacheisvalidthenwecanjustimmediatelysendthe
  • //cacheddataandbedone.
  • if(self.cacheValid){
  • [subscribersendNext:self.cachedData];
  • [subscribersendCompleted];
  • }else{
  • [subscribersendError:self.staleCacheError];
  • }
  • }]
  • //Dothesubscriptionworkonsomerandomscheduler,offthemain
  • //thread.
  • subscribeOn:[RACSchedulerscheduler]];
  • }
  • -(void)update{
  • [[[[self
  • loadData]
  • //Catchtheerrorfrom-loadData.Itmeansourcacheisstale.Update
  • //ourcacheandsaveit.
  • catch:^(NSError*error){
  • return[[selfupdateCachedData]doNext:^(iddata){
  • [selfcacheData:data];
  • }];
  • }]
  • //Ourworkupuntilnowhasbeenonabackgroundscheduler.Getour
  • //resultsdeliveredonthemainthreadsowecandoUIwork.
  • deliverOn:RACScheduler.mainThreadScheduler]
  • subscribeNext:^(iddata){
  • //UpdateyourUIbasedon`data`.
  • //Updateagainafter`updateInterval`secondshavepassed.
  • [[RACSignalinterval:updateInterval]take:1]subscribeNext:^(id_){
  • [selfupdate];
  • }];
  • }];
  • }
  • 检测用户名是否可用
    void)setupUsernameAvailabilityChecking{
  • RAC(self,availabilityStatus)=[[[RACObserve(self.userTemplate,username)
  • throttle:kUsernameCheckThrottleInterval]//throttle表示interval时间内如果有sendNext,则放弃该nextValue
  • map:^(NSString*username){
  • if(username.length==0)return[RACSignalreturn:@(UsernameAvailabilityCheckStatusEmpty)];
  • return[[[[[FIBAPIClientsharedInstance]
  • getUsernameAvailabilityFor:usernameignoreCache:NO]
  • map:^(NSDictionary*result){
  • NSNumber*existsNumber=result[@"exists"];
  • if(!existsNumber)return@(UsernameAvailabilityCheckStatusFailed);
  • UsernameAvailabilityCheckStatusstatus=[existsNumberboolValue]?UsernameAvailabilityCheckStatusUnavailable:UsernameAvailabilityCheckStatusAvailable;
  • return@(status);
  • }]
  • catch:^(NSError*error){
  • return:@(UsernameAvailabilityCheckStatusFailed)];
  • }]startWith:@(UsernameAvailabilityCheckStatusChecking)];
  • }]
  • switchToLatest];
  • }
  • 可以看到这里也使用了map + switchToLatest模式,这样就可以自动取消上一次的网络请求。
    startWith的内部实现是concat,这里表示先将状态置为checking,然后再根据网络请求的结果设置状态。
    使用takeUntil:来处理Cell的button点击
    这个上面已经提到过了。
    token过期后自动获取新的
    开发APIClient时,会用到AccessToken,这个Token过一段时间会过期,需要去请求新的Token。比较好的用户体验是当token过期后,自动去获取新的Token,拿到后继续上一次的请求,这样对用户是透明的。
          
          
  • RACSignal*requestSignal=[RACSignalcreateSignal:^RACDisposable*(id<RACSubscriber>subscriber){
  • //supposefirsttimesendrequest,accesstokenisexpiredorinvalid
  • //andnexttimeitiscorrect.
  • //theblockwillbetriggeredtwice.
  • staticBOOLisFirstTime=0;
  • NSString*url=@"http://httpbin.org/ip";
  • if(!isFirstTime){
  • url=@"http://nonexists.com/error";
  • isFirstTime=1;
  • }
  • NSLog(@"url:%@",url);
  • [[AFHTTPRequestOperationManagermanager]GET:urlparameters:nilsuccess:^(AFHTTPRequestOperation*operation,idresponseObject){
  • [subscribersendNext:responseObject];
  • [subscribersendCompleted];
  • }failure:^(AFHTTPRequestOperation*operation,NSError*error){
  • [subscribersendError:error];
  • }];
  • returnnil;
  • }];
  • self.statusLabel.text=@"sendingrequest...";
  • [[requestSignalcatch:^RACSignal*(NSError*error){
  • self.statusLabel.text=@"oops,invalidaccesstoken";
  • //simulatenetworkrequest,andwefetchtherightaccesstoken
  • return[[RACSignalcreateSignal:^RACDisposable*(id<RACSubscriber>subscriber){
  • doubledelayInSeconds=1.0;
  • dispatch_time_tpopTime=dispatch_time(DISPATCH_TIME_NOW,(int64_t)(delayInSeconds*NSEC_PER_SEC));
  • dispatch_after(popTime,dispatch_get_main_queue(),^(void){
  • [subscribersendNext:@YES];
  • [subscribersendCompleted];
  • });
  • returnnil;
  • }]concat:requestSignal];
  • }]subscribeNext:^(idx){
  • if([xisKindOfClass:[NSDictionaryclass]]){
  • self.statusLabel.text=[NSStringstringWithFormat:@"result:%@",x[@"origin"]];
  • }
  • }completed:^{
  • NSLog(@"completed");
  • }];
  • 注意事项
    RAC我自己感觉遇到的几个难点是: 1) 理解RAC的理念。 2) 熟悉常用的API。3) 针对某些特定的场景,想出比较合理的RAC处理方式。不过看多了,写多了,想多了就会慢慢适应。下面是我在实践过程中遇到的一些小坑。
    ReactiveCocoaLayout
    ReactiveCocoaLayout的使用好比「批地」和「盖房」,先通过insetWidth:height:nullRect从某个View中划出一小块,拿到之后还可以通过divideWithAmount:padding:fromEdge 再分成两块,或sliceWithAmount:fromEdge再分出一块。这些方法返回的都是signal,所以可以通过RAC(self.view,frame) = someRectSignal 这样来实现绑定。但在实践中发现性能不是很好,多批了几块地就容易造成主线程卡顿。
    所以ReactiveCocoaLayout最好不用或少用。
    调试
    刚开始写RAC时,往往会遇到这种情况,满屏的调用栈信息都是RAC的,要找出真正出现问题的地方不容易。曾经有一次在使用[RACSignal combineLatest: reduce:^id{}]时,忘了在Block里返回value,而Xcode也没有提示warning,然后就是莫名其妙地挂起了,跳到了汇编上,也没有调用栈信息,这时就只能通过最古老的注释代码的方式来找到问题的根源。
    不过写多了之后,一般不太会犯这种低级错误。
    strongify / weakify dance
    因为RAC很多操作都是在Block中完成的,这块最常见的问题就是在block直接把self拿来用,造成block和self的retain cycle。所以需要通过@strongify和@weakify来消除循环引用。
    有些地方很容易被忽略,比如RACObserve(thing,keypath),看上去并没有引用self,所以在subscribeNext时就忘记了weakify/strongify。但事实上RACObserve总是会引用self,即使target不是self,所以只要有RACObserve的地方都要使用weakify/strongify。
    小结
    以上是我在做花瓣客户端和side project时总结的一些经验,但愿能带来一些帮助,有误的地方也欢迎指正和探讨。
    推荐一下jspahrsummers的这个 project,虽然是用RAC3.0写的,但很多理念也可以用到RAC2上面。
    最后感谢Github的iOS工程师们,感谢你们带来了RAC,以及在Issues里的耐心解答。

    版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 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