理解ReactiveCocoa的map方法
ReactiveCocoa的map方法是一个直接体现函数式编程的标识性方法。
为了读懂map,我们要从ReactiveCocoa最基础的bind方法读起。
先从bind的用法说起,我们假设有一个信号,它连续发送数值1,2,3。我们希望使用bind得到一个新的信号,这个信号将原信号的发送出来的每个值都乘以2,那么,怎么写这个bind方法呢?
我们先来看看bind这个方法的签名如下:
- (RACSignal *)bind:(RACStreamBindBlock (^)(void))block
这个方法调用之后会返回一个新的信号(RACSignal)这个很好理解。
调用这个方法需要提供一个block,难点在于怎么提供这个block。
为了理解这个block,我们先写一个bind的调用框架如下:
// 先手写一个信号,它做的事情就是依次发送1,2,3然后结束。
RACSignal *orgSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@(1)];
[subscriber sendNext:@(2)];
[subscriber sendNext:@(3)];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
JCLogD(@"Original signal disposed");
}];
}];
// 然后我们希望通过bind得到一个新的信号,希望可以把原信号的数值乘以2。
RACSignal *newSignal = [orgSignal bind:^RACStreamBindBlock{
// return something...
}];
这个block是需要返回一个叫RACStreamBindBlock类型的东西。
再看看RACStreamBindBlock是什么东西?
它是这样定义的:
typedef RACStream * (^RACStreamBindBlock)(id value, BOOL *stop);
哦,是一个block,这个block有两个参数(id value, BOOL *stop);返回值是RACStream *。
现在我们可以更新一下bind的调用框架如下:
RACSignal *newSignal = [orgSignal bind:^RACStreamBindBlock{
RACStreamBindBlock bindBlock;
return bindBlock;
}];
好,现在就剩下怎么写bindBlock的值了
RACSignal *newSignal = [orgSignal bind:^RACStreamBindBlock{
RACStreamBindBlock bindBlock = ^(id value, BOOL *stop) {
RACSignal *signal = nil;
return signal;
};
return bindBlock;
}];
最后,我们把RACSignal的创建代码写完整了:
RACSignal *newSignal = [orgSignal bind:^RACStreamBindBlock{
RACStreamBindBlock bindBlock = ^(id value, BOOL *stop) {
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@(2*[value intValue])];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
JCLogD(@"Bind disposed");
}];
}];
return signal;
};
return bindBlock;
}];
最最后,我们把一些不必要的中间变量简化一下:
RACSignal *newSignal = [orgSignal bind:^RACStreamBindBlock {
return ^(id value, BOOL *stop) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@(2*[value intValue])];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
JCLogD(@"Bind disposed");
}];
}];
};
}];
每次一上来直接看这个版本的时候总是很费力:(,然后再回头看看上面的分解步骤就舒服了:)
再看看bind的功能注释:
-bind: should:
1. Subscribe to the original signal of values.
2. Any time the original signal sends a value, transform it using the binding block.
3. If the binding block returns a signal, subscribe to it, and pass all of its values through to the subscriber as they're received.
4. If the binding block asks the bind to terminate, complete the _original_ signal.
5. When _all_ signals complete, send completed to the subscriber.
If any signal sends an error at any point, send that to the subscriber.
大概的意思,对原信号进行订阅,对信号产生的每个值执行block进行转换,如果block是返回一个新的信号,立刻订阅这个信号,将订阅接收到的值发送给订阅者,如果block要求终止,则结束原信号(不是给订阅者发结束)。当原信号的所有值都发送完毕,就给订阅者发送完成。另外,任何时候信号产生error都会发送给订阅者
实现上面用bind实现的功能,其实,只需要一个简单的map方法即可,代码如下:
RACSignal *newSignal = [orgSignal map:^id(id value) {
return @(2*[value intValue]);
}];
那么问题来了,map的内部是怎么做到的呢?
请看代码(删除了一些干扰代码):
- (instancetype)map:(id (^)(id value))block {
Class class = self.class;
return [self flattenMap:^(id value) {
return [class return:block(value)];
}];
}
这个代码十分简单,唯一可能让你困惑的是其中叫return
的函数名,它是一个函数名,不是一个关键字!
看来,map
就是调用了flattenMap
来实现功能的嘛。
那么再看flattenMap的实现代码吧(删除了一些干扰代码):
- (instancetype)flattenMap:(RACStream * (^)(id value))block {
Class class = self.class;
return [self bind:^{
return ^(id value, BOOL *stop) {
id stream = block(value) ?: [class empty];
return stream;
};
}];
}
哦,它就是调用bind来实现功能的嘛!
但是....
怎么看不见flattenMap调用bind的时候有创建什么信号,然后返回信号呢?
我们细细看看:
- 它返回的是stream
- 这个stream是block(value)的执行返回结果
- 这个block是通过map传进来的
- 我们要回头看看map传了个什么block给flattenMap
- 哦,调用flattenMap时候传进来的block是这个:
^(id value) {
return [class return:block(value)];
}
- 这个block执行的结果是
[class return:];
的返回值
好吧,看了半天,基本可以确认是上面这个代码返回了一个RACSignal(之类)的东西了。
嗯,是的,代码就在这里:
@implementation RACSignal (RACStream)
+ (RACSignal *)empty {
return [RACEmptySignal empty];
}
+ (RACSignal *)return:(id)value {
return [RACReturnSignal return:value];
}
(为了找到这个代码还是找了半天的,因为这个坑爹的函数名,让Xcode认为它是一个关键字)
RACReturnSignal就没什么神秘的,它就是一个RACSignal的派生类
源代码里的注释写的很清楚:
// A private RACSignal
subclasses that synchronously sends a value to any
// subscribers, then completes.
PS: 虽然RACReturnSignal是一个“内部”的类,我们不能直接使用,我们是可以在RACSignal这个类上使用return
方法来简化我们的bind实现:
RACSignal *newSignal = [orgSignal bind:^RACStreamBindBlock{
return ^(id value, BOOL *stop) {
return [RACSignal return:@(2 * [value intValue])];
};
}];
补充:
- bind是为了在处理过程中可以手动终止而存在的,如果你不需要手动终止,那么,使用flattenMap才是合适的选择
- bind所做的一切都是为了lazy,也就是说bind出来的信号,如果不被订阅,一起计算都不会发生。
- bind的实现说明有一个不是很明白的地方*stop的效果,说明里说会“complete the original signal”,不知道是什么意思?