<audio id="audio" title="22 | 细说 iOS 响应式框架变迁，哪些思想可以为我所用？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/01/58/01bca264cf4bc987fc734fdf06227a58.mp3"></audio>

你好，我是戴铭。

说到iOS 响应式框架，最开始被大家知道的是 ReactiveCocoa（简称RAC），后来比较流行的是 RxSwift。但据我了解，iOS原生开发使用 ReactiveCocoa框架的团队并不多，而前端在推出React.js 后，响应式思路遍地开花。

那么，**响应式框架到底是什么，为什么在iOS原生开发中没被广泛采用，却能在前端领域得到推广呢？**

我们先来看看响应式框架，它指的是能够支持响应式编程范式的框架。使用了响应式框架，你在编程时就可以使用数据流传播数据的变化，响应这个数据流的计算模型会自动计算出新的值，将新的值通过数据流传给下一个响应的计算模型，如此反复下去，直到没有响应者为止。

React.js框架的底层有个 Virtual DOM（虚拟文档对象模型），页面组件状态会和 Virtual DOM 绑定，用来和 DOM（文档对象模型）做映射与转换。当组件状态更新时，Virtual DOM 就会进行 Diff 计算，最终只将需要渲染的节点进行实际 DOM 的渲染。

JavaScript 每次操作 DOM 都会全部重新渲染，而Virtual DOM 相当于 JavaScript 和 DOM 之间的一个缓存，JavaScript 每次都是操作这个缓存，对其进行 Diff 和变更，最后才将整体变化对应到 DOM 进行最后的渲染，从而减少没必要的渲染。

React.js 的 Virtual DOM 映射和转换 DOM 的原理，如下图所示。我们一起通过原理，来分析一下它的性能提升。

<img src="https://static001.geekbang.org/resource/image/67/a2/672e07e4347b132701c37d21ac7a44a2.png" alt=""><br>
可以看出，操作 Virtual DOM 时并不会直接进行 DOM 渲染，而是在完成了 Diff 计算得到所有实际变化的节点后才会进行一次 DOM 操作，然后整体渲染。而 DOM 只要有操作就会进行整体渲染。

直接在 DOM 上进行操作是非常昂贵的，所以视图组件会和 Virtual DOM 绑定，状态的改变直接更改 Virtual DOM。Virtual DOM 会检查两个状态之间的差异，进行最小的修改，所以 React.js 具有很好的性能。也正是因为性能良好，React.js才能够在前端圈流行起来。

而反观iOS，ReactiveCocoa框架的思路，其实与React.js中页面组件状态和 Virtual DOM 绑定、同步更新的思路是一致的。那**为什么 ReactiveCocoa 在iOS原生开发中就没流行起来呢？**

我觉得，主要原因是前端DOM 树的结构非常复杂，进行一次完整的 DOM 树变更，会带来严重的性能问题，而有了 Virtual DOM 之后，不直接操作 DOM 可以避免对整个 DOM 树进行变更，使得我们不用再担忧应用的性能问题。

但是，这种性能问题并不存在于iOS 原生开发。这，主要是得易于 Cocoa Touch 框架的界面节点树结构要比 DOM 树简单得多，没有前端那样的历史包袱。

与前端 DOM 渲染机制不同，Cocoa Touch 每次更新视图时不会立刻进行整个视图节点树的重新渲染，而是会通过 setNeedsLayout 方法先标记该视图需要重新布局，直到绘图循环到这个视图节点时才开始调用 layoutSubviews 方法进行重新布局，最后再渲染。

所以说，ReactiveCocoa框架并没有为 iOS 的 App 带来更好的性能。当一个框架可有可无，而且没有明显收益时，一般团队是没有理由去使用的。那么，像 ReactiveCocoa 这种响应式思想的框架在 iOS 里就没有可取之处了吗？

我觉得并不是。今天，我就来跟你分享下，**ReactiveCocoa 里有哪些思想可以为我所用，帮我们提高开发效率？**

ReactiveCocoa 是将函数式编程和响应式编程结合起来的库，通过函数式编程思想建立了数据流的通道，数据流动时会经过各种函数的处理最终到达和数据绑定的界面，由此实现了数据变化响应界面变化的效果。

## Monad

ReactiveCocoa 是采用号称纯函数式编程语言里的 Monad 设计模式搭建起来的，核心类是 RACStream。我们使用最多的 RACSignal（信号类，建立数据流通道的基本单元） ，就是继承自RACStream。RACStream 的定义如下：

```
typedef RACStream * (^RACStreamBindBlock)(id value, BOOL *stop);

/// An abstract class representing any stream of values.
///
/// This class represents a monad, upon which many stream-based operations can
/// be built.
///
/// When subclassing RACStream, only the methods in the main @interface body need
/// to be overridden.
@interface RACStream : NSObject

+ (instancetype)empty;
+ (instancetype)return:(id)value;
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;
- (instancetype)concat:(RACStream *)stream;
- (instancetype)zipWith:(RACStream *)stream;

@end

```

通过定义的注释可以看出，RACStream的作者也很明确地写出了RACStream 类表示的是一个 Monad，所以我们在 RACStream 上可以构建许多基于数据流的操作；RACStreamBindBlock，就是用来处理 RACStream 接收到数据的函数。那么，**Monad 就一定是好的设计模式吗？**

**从代码视觉上看**，Monad 为了避免赋值语句做了很多数据传递的管道工作。这样的话，我们在分析问题时，就很容易从代码层面清晰地看出数据流向和变化。而如果是赋值语句，在分析数据时就需要考虑数据状态和生命周期，会增加调试定位的成本，强依赖调试工具去观察变量。

**从语言发展来看**，Monad 虽然可以让上层接口看起来很简洁，但底层的实现却犹如一团乱麻。为了达到“纯”函数效果，Monad底层将各种函数的参数和返回值封装在了类型里，将本来可以通过简单数据赋值给变量记录的方式复杂化了。

不过无论是赋值方式还是 Monad 方式，编译后生成的代码都是一样的。王垠在他的博文“[函数式语言的宗教](http://www.yinwang.org/blog-cn/2013/03/31/purely-functional)”里详细分析了 Monad，并且写了两段分别采用赋值和函数式的代码，编译后的机器码实际上是一样的。如果你感兴趣的话，可以看一下这篇文章。

所以，如果你不想引入 ReactiveCocoa 库，还想使用函数响应式编程思想来开发程序的话，完全不用去重新实现一个采用 Monad 模式的 RACStream，只要在上层按照函数式编程的思想来搭建数据流管道，在下层使用赋值方式来管理数据就可以了。并且，采用这种方式，可能会比 Monad 这种“纯”函数来得更加容易。

## 函数响应式编程例子

接下来，我通过一个具体的案例来和你说明下，如何搭建一个不采用 Monad 模式的函数响应式编程框架。

这个案例要完成的功能是：添加学生基本信息，添加完学生信息后，通过按钮点击累加学生分数，每次点击按钮分数加5；所得分数在30分内，颜色显示为灰色；分数在30到70分之间，颜色显示为紫色；分数在70分内，状态文本显示不合格；超过70分，分数颜色显示为红色，状态文本显示合格。初始态分数为0，状态文本显示未设置。

这个功能虽然不难完成，但是如果我们将这些逻辑都写在一起，那必然是条件里套条件，当要修改功能时，还需要从头到尾再捋一遍。

如果把逻辑拆分成小逻辑放到不同的方法里，当要修改功能时，查找起来也会跳来跳去，加上为了描述方法内逻辑，函数名和参数名也需要非常清晰。这，无疑加重了开发和维护成本，特别是函数里面的逻辑被修改了后，我们还要对应着修改方法名。否则，错误的方法名，将会误导后来的维护者。

那么，**使用函数响应式编程方式会不会好一些呢？**

这里，我给出了使用函数响应式编程方式的代码，你可以对比看看是不是比条件里套条件和方法里套方法的写法要好。

**首先，**创建一个学生的记录，在创建记录的链式调用里添加一个处理状态文本显示的逻辑。代码如下：

```
// 添加学生基本信息
self.student = [[[[[SMStudent create]
                   name:@&quot;ming&quot;]
                  gender:SMStudentGenderMale]
                 studentNumber:345]
                filterIsASatisfyCredit:^BOOL(NSUInteger credit){
                    if (credit &gt;= 70) {
                        // 分数大于等于 70 显示合格
                        self.isSatisfyLabel.text = @&quot;合格&quot;;
                        self.isSatisfyLabel.textColor = [UIColor redColor];
                        return YES;
                    } else {
                        // 分数小于 70 不合格
                        self.isSatisfyLabel.text = @&quot;不合格&quot;;
                        return NO;
                    }
                }];

```

可以看出，当分数小于70时，状态文本会显示为“不合格”，大于等于70时会显示为“合格”。

**接下来，**针对分数，我再创建一个信号，当分数有变化时，信号会将分数传递给这个分数信号的两个订阅者。代码如下：

```
// 第一个订阅的credit处理
[self.student.creditSubject subscribeNext:^(NSUInteger credit) {
    NSLog(@&quot;第一个订阅的credit处理积分%lu&quot;,credit);
    self.currentCreditLabel.text = [NSString stringWithFormat:@&quot;%lu&quot;,credit];
    if (credit &lt; 30) {
        self.currentCreditLabel.textColor = [UIColor lightGrayColor];
    } else if(credit &lt; 70) {
        self.currentCreditLabel.textColor = [UIColor purpleColor];
    } else {
        self.currentCreditLabel.textColor = [UIColor redColor];
    }
}];

// 第二个订阅的credit处理
[self.student.creditSubject subscribeNext:^(NSUInteger credit) {
    NSLog(@&quot;第二个订阅的credit处理积分%lu&quot;,credit);
    if (!(credit &gt; 0)) {
        self.currentCreditLabel.text = @&quot;0&quot;;
        self.isSatisfyLabel.text = @&quot;未设置&quot;;
    }
}];

```

可以看出，这两个分数信号的订阅者分别处理了两个功能逻辑：

- 第一个处理的是分数颜色；
- 第二个处理的是初始状态下状态文本的显示逻辑。

整体看起来，所有的逻辑都围绕着分数这个数据的更新自动流动起来，也能够很灵活地通过信号订阅的方式进行归类处理。

采用这种编程方式，上层实现方式看起来类似于 ReactiveCocoa，而底层实现却非常简单，将信号订阅者直接使用赋值的方式赋值给一个集合进行维护，而没有使用 Monad 方式。底层对信号和订阅者的实现代码如下所示：

```
@interface SMCreditSubject : NSObject

typedef void(^SubscribeNextActionBlock)(NSUInteger credit);

+ (SMCreditSubject *)create;

// 发送信号
- (SMCreditSubject *)sendNext:(NSUInteger)credit;
// 接收信号
- (SMCreditSubject *)subscribeNext:(SubscribeNextActionBlock)block;

@end

@interface SMCreditSubject()

@property (nonatomic, assign) NSUInteger credit; // 积分
@property (nonatomic, strong) SubscribeNextActionBlock subscribeNextBlock; // 订阅信号事件
@property (nonatomic, strong) NSMutableArray *blockArray; // 订阅信号事件队列

@end

@implementation SMCreditSubject

// 创建信号
+ (SMCreditSubject *)create {
    SMCreditSubject *subject = [[self alloc] init];
    return subject;
}

// 发送信号
- (SMCreditSubject *)sendNext:(NSUInteger)credit {
    self.credit = credit;
    if (self.blockArray.count &gt; 0) {
        for (SubscribeNextActionBlock block in self.blockArray) {
            block(self.credit);
        }
    }
    return self;
}

// 订阅信号
- (SMCreditSubject *)subscribeNext:(SubscribeNextActionBlock)block {
    if (block) {
        block(self.credit);
    }
    [self.blockArray addObject:block];
    return self;
}

#pragma mark - Getter
- (NSMutableArray *)blockArray {
    if (!_blockArray) {
        _blockArray = [NSMutableArray array];
    }
    return _blockArray;
}

```

如上面代码所示，订阅者都会记录到 blockArray 里，block 的类型是 SubscribeNextActionBlock。

最终，我们使用函数式编程的思想，简单、高效地实现了这个功能。这个例子完整代码，你可以点击[这个链接](https://github.com/ming1016/RACStudy)查看。

## 小结

今天这篇文章，我和你分享了ReactiveCocoa 这种响应式编程框架难以在 iOS 原生开发中流行开的原因。

从本质上看，响应式编程没能提高App的性能，是其没能流行起来的主要原因。

在调试上，由于 ReactiveCocoa框架采用了 Monad 模式，导致其底层实现过于复杂，从而在方法调用堆栈里很难去定位到问题。这，也是ReactiveCocoa没能流行起来的一个原因。

但， ReactiveCocoa的上层接口设计思想，可以用来提高代码维护的效率，还是可以引入到 iOS 开发中的。

ReactiveCocoa里面还有很多值得我们学习的地方，比如说宏的运用。对此感兴趣的话，你可以看看sunnyxx的那篇[《Reactive Cocoa Tutorial [1] = 神奇的Macros》。](http://blog.sunnyxx.com/2014/03/06/rac_1_macros/)

对于 iOS 开发来说，响应式编程还有一个很重要的技术是 KVO，使用 KVO 来实现响应式开发的范例可以参考[我以前的一个 demo](https://github.com/ming1016/DecoupleDemo)。如果你有关于KVO的问题，也欢迎在评论区给我留言。

## 课后作业

在今天这篇文章里面，我和你聊了Monad 的很多缺点，不知道你是如何看待Monad的，在评论区给我留言分享下你的观点吧。

感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎把它分享给更多的朋友一起阅读。


