响应式编程框架ReactiveCocoa介绍与入门

前端之家收集整理的这篇文章主要介绍了响应式编程框架ReactiveCocoa介绍与入门前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

ReactiveCocoa是Github团队开发的第三方函数式响应式编程框架,在目前市面上的很多iOS App都大量使用了这个框架。以下我简称这个框架为RAC.我下面会通过几篇博客来和大家一起学习这个强大的框架。该博客的案例代码已经上传https://github.com/chenyufeng1991/ReactiveCocoaDemo 。当然最好的学习方式是去阅读RAC的源码,Github上面RAC的官网地址 https://github.com/ReactiveCocoa/ReactiveCocoa 。在官网中,包含了源码,代码示例,文档。在本篇博客中,我主要是对官方文档进行翻译,并加入自己的理解与实现。这里实现的语言为OC。

【1】ReactiveCocoa(RAC)介绍

RAC是iOS的一个函数式响应式编程框架,而不是使用可变的变量去修改和替换原有的值。RAC提供了信号(RACSignal类)来监听当前和未来的值。通过信号的链接、组合和响应,可以让我们的代码持续的观察和更新值。我用一句话说就是:响应数据的变化。

举个例子,我们可以绑定一个TextField输入框,只要绑定的值有改变,我们可以不添加任何额外的代码,就可以更新该输入框。工作原理类似于KVO,但是使用block块来替代重写“observeValueForKeyPath:ofObject:change:context”这个方法。信号也代表了异步操作,可以简化网络请求等异步代码。RAC的一个最主要优势就是提供了信号,统一处理了iOS中的异步行为,包括delegate,block回调,target-action机制,Notification和KVO。如下的例子:

// When self.username changes,logs the new name to the console.
//
// RACObserve(self,username) creates a new RACSignal that sends the current
// value of self.username,then the new value whenever it changes.
// -subscribeNext: will execute the block whenever the signal sends a value.
[RACObserve(self,username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@",newName);
}];

当self.username的值改变时,log中就会输出新的值。RACObserve创建了一个新的RACSignal对象,可以发送最新的值到self.username,因此值就会随时改变。当信号signal发送新的值时,-subscribeNext就会执行block块中的代码

但是和KVO不一样,信号可以被链起来并操作,如下代码所示:

// Only logs names that starts with "j".
//
// -filter returns a new RACSignal that only sends a new value when its block
// returns YES.
[[RACObserve(self,username)
    filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }]
    subscribeNext:^(NSString *newName) {
        NSLog(@"%@",newName);
    }];

上面的log中只会输出包含前缀为j的字符串。-filter会返回新的RACSignal对象,可以根据block返回新的值。

信号同样可以用来得到状态,可以很方便的给属性一个信号和操作。如下代码所示:

// Creates a one-way binding so that self.createEnabled will be
// true whenever self.password and self.passwordConfirmation
// are equal.
//
// RAC() is a macro that makes the binding look nicer.
// 
// +combineLatest:reduce: takes an array of signals,executes the block with the
// latest value from each signal whenever any of them changes,and returns a new
// RACSignal that sends the return value of that block as values.
RAC(self,createEnabled) = [RACSignal 
    combineLatest:@[ RACObserve(self,password),RACObserve(self,passwordConfirmation) ] 
    reduce:^(NSString *password,NSString *passwordConfirm) {
        return @([passwordConfirm isEqualToString:password]);
    }];

以上代码创建了一种新的数据绑定的方式,当self.password和self.passwordConfirmation相等的时候会返回true。RAC()是宏,可以让数据绑定看起来更加良好。+combineLatest:reduce: 是信号的数组,只要任意一个信号中的值有改变,就会用最新的值去执行block中的代码,然后返回新的RACSignal对象,用来发送新值。

信号可以随时创建在任何值的流上,不同于KVO。举个例子,信号可以代表按钮点击:

// Logs a message whenever the button is pressed.
//
// RACCommand creates signals to represent UI actions. Each signal can
// represent a button press,for example,and have additional work associated
// with it.
//
// -rac_command is an addition to NSButton. The button will send itself on that
// command whenever it's pressed.
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
    NSLog(@"button was pressed!");
    return [RACSignal empty];
}];

每当按钮点击的时候就会输出日志。RACCommand创建了一个信号表示UI事件。每一个信号可以表示一个按钮点击,并可以执行相关的操作。同样的,RACCommand也可以进行异步网络操作,如下:
// Hooks up a "Log in" button to log in over the network.
//
// This block will be run whenever the login command is executed,starting
// the login process.
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
    // The hypothetical -logIn method returns a signal that sends a value when
    // the network request finishes.
    return [client logIn];
}];

// -executionSignals returns a signal that includes the signals returned from
// the above block,one for each time the command is executed.
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
    // Log a message whenever we log in successfully.
    [loginSignal subscribeCompleted:^{
        NSLog(@"Logged in successfully!");
    }];
}];

// Executes the login command when the button is pressed.
self.loginButton.rac_command = self.loginCommand;

上面的代码用来连接登录按钮和网络操作。当开始登录的时候将会去执行第一个block,在block中假设的logIn方法将会在网络请求结束的时候返回一个信号。-executeSignals将会返回信号,包括了上面第一个block中的信号。

使用信号也可以用来表示定时器,其他的UI事件,或者随着事件变化的操作。使用信号可以让复杂的异步操作通过链式和传递信号变得更加简单。当一组操作完成后信号就可以被触发,如下:

// Performs 2 network operations and logs a message to the console when they are
// both completed.
//
// +merge: takes an array of signals and returns a new RACSignal that passes
// through the values of all of the signals and completes when all of the
// signals complete.
//
// -subscribeCompleted: will execute the block when the signal completes.
[[RACSignal 
    merge:@[ [client fetchUserRepos],[client fetchOrgRepos] ]] 
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

当client的两个网路请求都完成后,控制台就会打印出信息。+merge:获得信号数组,当数组中的信号都完成后,返回RACSignal对象。在异步操作中,信号可以被链式然后按序列执行,而不用使用嵌套的block回调。如下所示:
// Logs in the user,then loads any cached messages,then fetches the remaining
// messages from the server. After that's all done,logs a message to the
// console.
//
// The hypothetical -logInUser methods returns a signal that completes after
// logging in.
//
// -flattenMap: will execute its block whenever the signal sends a value,and
// returns a new RACSignal that merges all of the signals returned from the block
// into a single signal.
[[[[client 
    logInUser] 
    flattenMap:^(User *user) {
        // Return a signal that loads cached messages for the user.
        return [client loadCachedMessagesForUser:user];
    }]
    flattenMap:^(NSArray *messages) {
        // Return a signal that fetches any remaining messages.
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeNext:^(NSArray *newMessages) {
        NSLog(@"New messages: %@",newMessages);
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

用户登录,先加载缓存数据,然后从远程服务器抓取数据,以上操作完成后,打印log。 假设的-logInUser方法登录完成后会返回信号。 -flattenMap:方法当信号发送一个值的时候就会去执行block,并返回一个新的RACSignal对象,该对象会合并上面所有的信号为一个单一信号。RAC可以使绑定异步操作的结果更加简单:
// Creates a one-way binding so that self.imageView.image will be set as the user's
// avatar as soon as it's downloaded.
//
// The hypothetical -fetchUserWithUsername: method returns a signal which sends
// the user.
//
// -deliverOn: creates new signals that will do their work on other queues. In
// this example,it's used to move work to a background queue and then back to the main thread.
//
// -map: calls its block with each user that's fetched and returns a new
// RACSignal that sends values returned from the block.
RAC(self.imageView,image) = [[[[client 
    fetchUserWithUsername:@"joshaber"]
    deliverOn:[RACScheduler scheduler]]
    map:^(User *user) {
        // Download the avatar (this is done on a background queue).
        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
    }]
    // Now the assignment will be done on the main thread.
    deliverOn:RACScheduler.mainThreadScheduler];

创建了一个绑定,当用户头像下载完成后,self.imageView.image就会被立即设置。假设的-fetchUserWithUsername:会发送一个信号。 -deliverOn:创建一个信号可以让任务在其他队列中去执行。在这个例子中,是用来让任务在后台队列执行然后切换到主线程。

上面简单描述了RAC可以做的一些事情,但是没有说明为什么RAC如此强大。如果想看更多的示例代码,可以查看C-41GroceryList这两个项目,这两个项目都是用RAC来写的。


【何时使用RAC】

当第一次看到RAC的时候,感觉非常的抽象,理解起来也非常的困难,以致于很难在具体的问题中使用。这里有一些具体在哪些情况下使用RAC的建议:

1.处理异步任务或者事件驱动数据源的时候

大多数Cocoa的程序都是关注于响应用户的事件。但是处理此类事件的代码会很快变得很复杂,因为有大量的回调和状态变量。这种模式从表面上看起来都很不一样,像UI回调,网络响应,KVO,其实他们有很多都是共通的。RACSignal统一了这些不同的API,并让我们使用相同的方式来调用。下面代码

static void *ObservationContext = &ObservationContext;

- (void)viewDidLoad {
    [super viewDidLoad];

    [LoginManager.sharedManager addObserver:self forKeyPath:@"loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext];
    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(loggedOut:) name:UserDidlogoutNotification object:LoginManager.sharedManager];

    [self.usernameTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
    [self.passwordTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
    [self.logInButton addTarget:self action:@selector(logInPressed:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)dealloc {
    [LoginManager.sharedManager removeObserver:self forKeyPath:@"loggingIn" context:ObservationContext];
    [NSNotificationCenter.defaultCenter removeObserver:self];
}

- (void)updateLogInButton {
    BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0;
    BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn;
    self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn;
}

- (IBAction)logInPressed:(UIButton *)sender {
    [[LoginManager sharedManager]
        logInWithUsername:self.usernameTextField.text
        password:self.passwordTextField.text
        success:^{
            self.loggedIn = YES;
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
}

- (void)loggedOut:(NSNotification *)notification {
    self.loggedIn = NO;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == ObservationContext) {
        [self updateLogInButton];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

我们也可以把上述代码改写成RAC形式:
- (void)viewDidLoad {
    [super viewDidLoad];

    @weakify(self);

    RAC(self.logInButton,enabled) = [RACSignal
        combineLatest:@[
            self.usernameTextField.rac_textSignal,self.passwordTextField.rac_textSignal,RACObserve(LoginManager.sharedManager,loggingIn),loggedIn)
        ] reduce:^(NSString *username,NSString *password,NSNumber *loggingIn,NSNumber *loggedIn) {
            return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
        }];

    [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) {
        @strongify(self);

        RACSignal *loginSignal = [LoginManager.sharedManager
            logInWithUsername:self.usernameTextField.text
            password:self.passwordTextField.text];

            [loginSignal subscribeError:^(NSError *error) {
                @strongify(self);
                [self presentError:error];
            } completed:^{
                @strongify(self);
                self.loggedIn = YES;
            }];
    }];

    RAC(self,loggedIn) = [[NSNotificationCenter.defaultCenter
        rac_addObserverForName:UserDidlogoutNotification object:nil]
        mapReplace:@NO];
}


2.链的依赖操作

依赖在网络请求中很常见,比如下一个请求之前要先去完成前一个请求。比如:

[client logInWithSuccess:^{
    [client loadCachedMessagesWithSuccess:^(NSArray *messages) {
        [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
            NSLog(@"Fetched all messages.");
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
    } failure:^(NSError *error) {
        [self presentError:error];
    }];
} failure:^(NSError *error) {
    [self presentError:error];
}];
而RAC可以让这种模式变得简单,改造如下:
[[[[client logIn]
    then:^{
        return [client loadCachedMessages];
    }]
    flattenMap:^(NSArray *messages) {
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeError:^(NSError *error) {
        [self presentError:error];
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

3.并行独立任务

在并行任务中处理独立的数据集,并把它们组合成最后的结果,这样的操作往往会涉及大量的同步操作,我们常用的代码如下:

__block NSArray *databa@R_404_401@bjects;
__block NSArray *fileContents;

NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *databa@R_404_401@peration = [NSBlockOperation blockOperationWithBlock:^{
    databa@R_404_401@bjects = [databaseClient fetchObjectsMatchingPredicate:predicate];
}];

NSBlockOperation *filesOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSMutableArray *filesInProgress = [NSMutableArray array];
    for (NSString *path in files) {
        [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
    }

    fileContents = [filesInProgress copy];
}];

NSBlockOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{
    [self finishProcessingDataba@R_404_401@bjects:databa@R_404_401@bjects fileContents:fileContents];
    NSLog(@"Done processing");
}];

[finishOperation addDependency:databa@R_404_401@peration];
[finishOperation addDependency:filesOperation];
[backgroundQueue addOperation:databa@R_404_401@peration];
[backgroundQueue addOperation:filesOperation];
[backgroundQueue addOperation:finishOperation];

上面的代码可以优化为简单的组合信号,RAC后的代码如下:
RACSignal *databaseSignal = [[databaseClient
    fetchObjectsMatchingPredicate:predicate]
    subscribeOn:[RACScheduler scheduler]];

RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
    NSMutableArray *filesInProgress = [NSMutableArray array];
    for (NSString *path in files) {
        [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
    }

    [subscriber sendNext:[filesInProgress copy]];
    [subscriber sendCompleted];
}];

[[RACSignal
    combineLatest:@[ databaseSignal,fileSignal ]
    reduce:^ id (NSArray *databa@R_404_401@bjects,NSArray *fileContents) {
        [self finishProcessingDataba@R_404_401@bjects:databa@R_404_401@bjects fileContents:fileContents];
        return nil;
    }]
    subscribeCompleted:^{
        NSLog(@"Done processing");
    }];

4.简化集合操作

高阶函数如map,filter,fold/reduce是没有在Foundation框架中的,会导致循环的代码如下:

NSMutableArray *results = [NSMutableArray array];
for (NSString *str in strings) {
    if (str.length < 2) {
        continue;
    }

    NSString *newString = [str stringByAppendingString:@"foobar"];
    [results addObject:newString];
}
而使用RACSequence可以对Cocoa中的集合操作进行统一处理,改造代码如下:
RACSequence *results = [[strings.rac_sequence
    filter:^ BOOL (NSString *str) {
        return str.length >= 2;
    }]
    map:^(NSString *str) {
        return [str stringByAppendingString:@"foobar"];
    }];

【系统要求】

RAC要求OS X10.8+,iOS 8.0+.


【导入RAC】

个人推荐使用CocoaPods来导入RAC。可以查看C-41GroceryList这两个项目,这两个项目里面已经包含了RAC.


【独立开发】

如果独立的开发RAC而不是把它集成到一个项目中,你应该要去打开ReactiveCocoa.xcworkspace 而不是.xcodeproj.


【更多资料】

RAC是基于.NET的Reactive Extensions(Rx),很多Rx种的原理都可以应用到RAC中,下面是一些Rx的资源:

Reactive Extensions MSDN entry

Reactive Extensions for .NET Introduction

Rx - Channel 9 video

Reactive Extensions wiki

101 Rx Samples

Programming Reactive Extensions and LINQ


RAC和Rx都是一种函数式响应式编程(Functional Reactive Programming),下面是关于FRP的资源:

What is FRP? - Elm Language

What is Functions Reactive Programming - Stack Overflow

Specification for a Functional Reactive Language - Stack Overflow

Escape from Callback Hell

Principles of Reactive Programming on Coursera


本文大部分翻译自 :https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.5/README.md

猜你在找的React相关文章