响应式编程框架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。如下的例子:

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

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

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

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

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

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

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

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

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

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

每当按钮点击的时候就会输出日志。RACCommand创建了一个信号表示UI事件。每一个信号可以表示一个按钮点击,并可以执行相关的操作。同样的,RACCommand也可以进行异步网络操作,如下:
  1. // Hooks up a "Log in" button to log in over the network.
  2. //
  3. // This block will be run whenever the login command is executed,starting
  4. // the login process.
  5. self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
  6. // The hypothetical -logIn method returns a signal that sends a value when
  7. // the network request finishes.
  8. return [client logIn];
  9. }];
  10.  
  11. // -executionSignals returns a signal that includes the signals returned from
  12. // the above block,one for each time the command is executed.
  13. [self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
  14. // Log a message whenever we log in successfully.
  15. [loginSignal subscribeCompleted:^{
  16. NSLog(@"Logged in successfully!");
  17. }];
  18. }];
  19.  
  20. // Executes the login command when the button is pressed.
  21. self.loginButton.rac_command = self.loginCommand;

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

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

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

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

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

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

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


【何时使用RAC】

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

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

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

  1. static void *ObservationContext = &ObservationContext;
  2.  
  3. - (void)viewDidLoad {
  4. [super viewDidLoad];
  5.  
  6. [LoginManager.sharedManager addObserver:self forKeyPath:@"loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext];
  7. [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(loggedOut:) name:UserDidlogoutNotification object:LoginManager.sharedManager];
  8.  
  9. [self.usernameTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
  10. [self.passwordTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
  11. [self.logInButton addTarget:self action:@selector(logInPressed:) forControlEvents:UIControlEventTouchUpInside];
  12. }
  13.  
  14. - (void)dealloc {
  15. [LoginManager.sharedManager removeObserver:self forKeyPath:@"loggingIn" context:ObservationContext];
  16. [NSNotificationCenter.defaultCenter removeObserver:self];
  17. }
  18.  
  19. - (void)updateLogInButton {
  20. BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0;
  21. BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn;
  22. self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn;
  23. }
  24.  
  25. - (IBAction)logInPressed:(UIButton *)sender {
  26. [[LoginManager sharedManager]
  27. logInWithUsername:self.usernameTextField.text
  28. password:self.passwordTextField.text
  29. success:^{
  30. self.loggedIn = YES;
  31. } failure:^(NSError *error) {
  32. [self presentError:error];
  33. }];
  34. }
  35.  
  36. - (void)loggedOut:(NSNotification *)notification {
  37. self.loggedIn = NO;
  38. }
  39.  
  40. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  41. if (context == ObservationContext) {
  42. [self updateLogInButton];
  43. } else {
  44. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  45. }
  46. }

我们也可以把上述代码改写成RAC形式:
  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3.  
  4. @weakify(self);
  5.  
  6. RAC(self.logInButton,enabled) = [RACSignal
  7. combineLatest:@[
  8. self.usernameTextField.rac_textSignal,self.passwordTextField.rac_textSignal,RACObserve(LoginManager.sharedManager,loggingIn),loggedIn)
  9. ] reduce:^(NSString *username,NSString *password,NSNumber *loggingIn,NSNumber *loggedIn) {
  10. return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
  11. }];
  12.  
  13. [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) {
  14. @strongify(self);
  15.  
  16. RACSignal *loginSignal = [LoginManager.sharedManager
  17. logInWithUsername:self.usernameTextField.text
  18. password:self.passwordTextField.text];
  19.  
  20. [loginSignal subscribeError:^(NSError *error) {
  21. @strongify(self);
  22. [self presentError:error];
  23. } completed:^{
  24. @strongify(self);
  25. self.loggedIn = YES;
  26. }];
  27. }];
  28.  
  29. RAC(self,loggedIn) = [[NSNotificationCenter.defaultCenter
  30. rac_addObserverForName:UserDidlogoutNotification object:nil]
  31. mapReplace:@NO];
  32. }


2.链的依赖操作

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

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

3.并行独立任务

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

  1. __block NSArray *databaSEObjects;
  2. __block NSArray *fileContents;
  3.  
  4. NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
  5. NSBlockOperation *databaSEOperation = [NSBlockOperation blockOperationWithBlock:^{
  6. databaSEObjects = [databaseClient fetchObjectsMatchingPredicate:predicate];
  7. }];
  8.  
  9. NSBlockOperation *filesOperation = [NSBlockOperation blockOperationWithBlock:^{
  10. NSMutableArray *filesInProgress = [NSMutableArray array];
  11. for (NSString *path in files) {
  12. [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
  13. }
  14.  
  15. fileContents = [filesInProgress copy];
  16. }];
  17.  
  18. NSBlockOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{
  19. [self finishProcessingDatabaSEObjects:databaSEObjects fileContents:fileContents];
  20. NSLog(@"Done processing");
  21. }];
  22.  
  23. [finishOperation addDependency:databaSEOperation];
  24. [finishOperation addDependency:filesOperation];
  25. [backgroundQueue addOperation:databaSEOperation];
  26. [backgroundQueue addOperation:filesOperation];
  27. [backgroundQueue addOperation:finishOperation];

上面的代码可以优化为简单的组合信号,RAC后的代码如下:
  1. RACSignal *databaseSignal = [[databaseClient
  2. fetchObjectsMatchingPredicate:predicate]
  3. subscribeOn:[RACScheduler scheduler]];
  4.  
  5. RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
  6. NSMutableArray *filesInProgress = [NSMutableArray array];
  7. for (NSString *path in files) {
  8. [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
  9. }
  10.  
  11. [subscriber sendNext:[filesInProgress copy]];
  12. [subscriber sendCompleted];
  13. }];
  14.  
  15. [[RACSignal
  16. combineLatest:@[ databaseSignal,fileSignal ]
  17. reduce:^ id (NSArray *databaSEObjects,NSArray *fileContents) {
  18. [self finishProcessingDatabaSEObjects:databaSEObjects fileContents:fileContents];
  19. return nil;
  20. }]
  21. subscribeCompleted:^{
  22. NSLog(@"Done processing");
  23. }];

4.简化集合操作

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

  1. NSMutableArray *results = [NSMutableArray array];
  2. for (NSString *str in strings) {
  3. if (str.length < 2) {
  4. continue;
  5. }
  6.  
  7. NSString *newString = [str stringByAppendingString:@"foobar"];
  8. [results addObject:newString];
  9. }
而使用RACSequence可以对Cocoa中的集合操作进行统一处理,改造代码如下:
  1. RACSequence *results = [[strings.rac_sequence
  2. filter:^ BOOL (NSString *str) {
  3. return str.length >= 2;
  4. }]
  5. map:^(NSString *str) {
  6. return [str stringByAppendingString:@"foobar"];
  7. }];

【系统要求】

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

@L_403_15@

Principles of Reactive Programming on Coursera


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

猜你在找的React相关文章