废话说在前面
不知道有多少人和我一样, 觉得现在学习Swift仍然是比较尴尬的: 第一是因为Swift目前还在不断进化中, 即使学习了也仍然会有大大小小的改动, 当然这个原因是比较次要的, 毕竟并不是每次升级都会面目全非, 而且相比较用Swift所带来的好处来说, 这点原因基本上可以被无视掉; 第二个原因也是最主要的, 大多数情况下, 我们的工程并不是全新的工程, 都有少则一年, 多则好几年的ObjC代码沉淀, 这部分代码不是说放弃就能放弃掉的, 当然, 我们可以混编, 但是另一个问题出现了, 假如团队内部并不是所有的人都对Swift感兴趣, 我们独自用Swift来写代码其实是有点不负责任的, 因为这对后面维护代码的人来说提出了更高的要求.
但是, 我们又不能一点准备都不做, 毕竟假如某天苹果开始强制要求所有的新应用或者版本升级必须用Swift来实现(例如Carbon?)那时候我们再来开始学习就比较悲剧了. 那么这样就出现矛盾了, 如果新学的东西不能在实际中应用上, 那被遗忘的速度会是很快的, 而且也不利于持续学习, 因此, 我个人认为, 先学编程思想, 以后再切换就是纯语法的的熟悉而已了.
ReactiveCocoa是ObjC界相当有名且成熟的库, 但是貌似在国内使用的人真的不多, 我问过旁边很多人对这个库的看法, 大多数都是觉得比较难用. 这很好理解, 毕竟ReactiveCocoa的编程思想就是函数响应式, 与ObjC这种面向对象的编程思想是有一定鸿沟的, 而且函数式编程本身学习曲线就比较陡峭, 所以就更加不被大多数人接受了.
之所以冒出想研究一下ReactiveCocoa的想法主要是两件事情, 第一件事是最近在写JS代码时, 突发奇想用了map函数, 觉得很好用, 就想到了函数式编程, 第二件事是native代码的一个bug让我想到如果那个时候用的是响应式编程, 就不会出现这个问题了. 因此这也让我下定决定要好好研究一下这个库.
文章构成
这会是一个系列文章, 但是什么时候更新真不好说, 因为最近的项目并行的比较多, 所以没有太多的时间来学习和沉淀. 另外, 如上所述, 我不是精通, 也是在学习当中, 希望自己一步步学习的过程能够给更多人带来一些共鸣, 和先行者的经验.
我会看情况翻译一些文档, 其实可以直接看github上的, 对基础用法和内存管理已经整体的框架介绍都有写. 另外会在分析源码的基础上来实验这些源码的作用. 最后我觉得也是最主要的, 希望能够和大家一起讨论, 什么地方用这个库会比较好, 大多数时候我们知道了某种编程思想, 但是不代表能够正确运用(毕竟还有不少人用着面向对象的语言, 写着面向过程的代码, 比如我写JS, 哈哈哈哈)
ReactiveCocoa库组成
代码下下来之后可以看到整个库主要由两部分组成----Core和UI, 我粗看了一下, UI部分使用起来其实和BlockKit很像, 但是本质上是完全不一样的, 所以我建议先从本质看起, 我们先看Core.
为了让大家找到熟悉的感觉(默认大家都看过一点Swift), 我从NSArray + RACSequenceAdditions这个Category开始
NSArray的Category
先来看看这个Category的.h和.m文件:
// .h
@interface NSArray (RACSequenceAdditions)
@property (nonatomic, copy, readonly) RACSequence *rac_sequence;
@end
// .m
@implementation NSArray (RACSequenceAdditions)
- (RACSequence *)rac_sequence {
return [RACArraySequence sequenceWithArray:self offset:0];
}
@end
非常简单, 只有一个RACSequence类型的属性, 在实现文件中直接返回一个RACArraySequence类型的实例.
RACArraySequence
很明显RACSequence/RACArraySequence是这个拓展的核心, 我们看了子类之后发现, 相比父类RACSequence, RACArraySequence多了2个私有属性, 除了实例获取方法, 其余全部继承自父类:
@interface RACArraySequence ()
@property (nonatomic, copy, readonly) NSArray *backingArray;
@property (nonatomic, assign, readonly) NSUInteger offset;
@end
通过追踪NSArray的Category中所调用的方法就知道 backingArray是array的一份copy:
+ (instancetype)sequenceWithArray:(NSArray *)array offset:(NSUInteger)offset {
NSCParameterAssert(offset <= array.count);
// 这里的empty后面会讲, 相当于nil
if (offset == array.count) return self.empty;
RACArraySequence *seq = [[self alloc] init];
seq->_backingArray = [array copy];
seq->_offset = offset;
return seq;
}
而offset则可以通过head(这个方法和tail会之后细讲)和array的返回值知道是起始位置的偏移量:
- (id)head {
return self.backingArray[self.offset];
}
- (NSArray *)array {
return [self.backingArray subarrayWithRange:NSMakeRange(self.offset, self.backingArray.count - self.offset)];
}
RACArraySequence.m文件中其余方法都是一些常规方法, 需要注意的是
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id[])stackbuf count:(NSUInteger)len
这个方法是因为RACArraySequence类支持快速遍历, 所以必须要实现, 外部不显式调用, 在for in循环的时候会调用到.
至此, RACArraySequence类就差不多了, 我们可以把目光移到其父类RACSequence上了.
RACSequence
从RACSequence.h中我们知道, RACSequence继承自RACStream, 我们暂时先不管, 先看看它暴露的接口:
@property (nonatomic, strong, readonly) id head;
@property (nonatomic, strong, readonly) RACSequence *tail;
@property (nonatomic, copy, readonly) NSArray *array;
@property (nonatomic, copy, readonly) NSEnumerator *objectEnumerator;
@property (nonatomic, copy, readonly) RACSequence *eagerSequence;
@property (nonatomic, copy, readonly) RACSequence *lazySequence;
- (RACSignal *)signal;
- (RACSignal *)signalWithScheduler:(RACScheduler *)scheduler;
- (id)foldLeftWithStart:(id)start reduce:(id (^)(id accumulator, id value))reduce;
- (id)foldRightWithStart:(id)start reduce:(id (^)(id first, RACSequence *rest))reduce;
- (BOOL)any:(BOOL (^)(id value))block;
- (BOOL)all:(BOOL (^)(id value))block;
- (id)objectPassingTest:(BOOL (^)(id value))block;
+ (RACSequence *)sequenceWithHeadBlock:(id (^)(void))headBlock tailBlock:(RACSequence *(^)(void))tailBlock;
方法比较多, 虽然源文件中每一个都有注释, 但是实际上看起来还是比较难理解的(可能是我英语太渣), 因此, talk is cheap, show the code.
插一句, 建议新建一个pod工程, 专门来写这些测试代码.
head
NSArray *numbers = @[@1, @2, @3];
NSLog(@"%@", numbers.rac_sequence.head);
// 无论从字面上还是之前RACArraySequence的代码中都可以猜到会打印出 1
tail
tail方法并不与head相对应地指向一个数组的尾部, 先看输出:
NSLog(@"%@", numbers.rac_sequence.tail);
// 打印如下:
<RACArraySequence: 0x7ff2925564d0>{ name = , array = (
1,
2,
3
) }
结合代码来看:
//RACArraySequence.m
- (RACSequence *)tail {
RACSequence *sequence = [self.class sequenceWithArray:self.backingArray offset:self.offset + 1];
sequence.name = self.name;
return sequence;
}
所以tail还是会返回一个Sequence, 只是他的偏移量会加1. 等等, 貌似哪里不对, 不是说了偏移量会加1了吗, 为什么输出的Array还是1,2,3? 这个输出是自定义的, 所以我们看看RACArraySequence.m的description方法:
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>{ name = %@, array = %@ }", self.class, self, self.name, self.backingArray];
}
o(╯□╰)o, 这里直接输出了bakingArray, 而没有显示偏移量, 所以个人感觉这里还是需要优化一下的.
为了得到我们想要的结果, 这样既可:
NSLog(@"%@", numbers.rac_sequence.tail.array);
// 打印出:
(
2,
3
)
所以, 这下我们知道了, 所谓的tail函数, 就是返回一个相遇于原sequence的偏移量+1的sequence
array
从tail中我们也知道了, array的作用就是把sequence中的元素取出来而已
objectEnumerator
一个遍历器, 实际返回NSEnumerator的子类RACSequenceEnumerator, 这个以后再讲吧, 不属于NSArray的核心, 毕竟我们一般都不直接使用NSEnumerator来遍历.
eagerSequence与lazySequence
所有的sequence都默认是lazy的, 为什么这么说, 直接看代码:
- (RACSequence *)lazySequence {
return self;
}
这与Swift的实现是一致的, 只有当使用到的时候再进行计算, 这会在后面说话某些处理函数时可以看到, 现在稍安勿躁. 至于eagerSequence则是对立面, 对元素的函数处理会立即执行, 而不是使用时再执行. 它返回一个子类RACEagerSequence, 这个在后面再讲.
signal与signalWithScheduler:
这个牵涉到具体的信号订阅了--可以理解为KVO--所以以后到UI部分再讲会适合.
foldLeftWithStart:reduce与foldRightWithStart:reduce
支持函数式编程的语言--包括Swift--都有reduce函数数的用, 在ReactiveCocoa中这两个函数的区别则是一个从左开始, 一个从右开始.
这里先讲一下reduce函数的作用吧:
假设数组中有3个元素, [1,2,3], reduce的作用则是给定某个初始值i和一个处理函数p, 然后对数组进行遍历, 每次都是上次p函数执行的结果加上一个新值. 可能比较难理解, 用数学表达式就是:
result = p(p(p(i,1), 2), 3)
假设i=0, p函数是一个加法器, 把2个数相加, 结果就会是:
(((0+1) + 2) + 3) = 6
这也就是为什么reduce函数需要一个初始值, 这个初始值需要要作为第一次函数执行的第一个参数.
而是ReactiveCocoa中的这2个函数则是方向不一样, 一个往左, 一个往右:
// 往左reduce
id obj = [numbers.rac_sequence foldLeftWithStart:@"start" reduce:^id(id accumulator, id value) {
return [NSString stringWithFormat:@"%@|%@", accumulator, value];
}];
NSLog(@"%@", obj); // 打印: start|1|2|3
// 往右reduce
obj = [numbers.rac_sequence foldRightWithStart:@"start" reduce:^id(id first, RACSequence *rest) {
return [NSString stringWithFormat:@"%@|%@", rest.head, first];
}];
NSLog(@"%@", obj); // 打印: start|3|2|1
any:与all:
这2个是测试型函数, 前者校验集合中是否有元素满足条件, 后者则是是否全部满足条件:
BOOL result = [numbers.rac_sequence any:^BOOL(id value) {
return [value integerValue] > 4;
}];
NSLog(result?@"Some number greater than 4":@"No number greater than 4");
// 打印: No number greater than 4
result = [numbers.rac_sequence all:^BOOL(id value) {
return [value integerValue] < 4;
}];
NSLog(result?@"All numbers less than 4":@"No number less than 4");
// 打印: All numbers less than 4
objectPassingTest:
这个是all:与any:最终调用的函数, 里面有调用了filter:, 满足条件的加入返回数组, 不满足的过滤掉, 里面也是最终调用了flatMap, 这个之后再讲.
如果有多个满足条件, 这个函数会返回第一个.
sequenceWithHeadBlock: tailBlock
通过sequence的源码我们可以知道, 如果我们能够定义出一个sequence的head和tail, 那么我们就可以定义出来一个sequence, 所以这就是这个方法的作用:
RACSequence *customSequence = [RACSequence sequenceWithHeadBlock:^id{
return @1;
} tailBlock:^RACSequence *{
return [@[@2] rac_sequence];
}];
NSLog(@"%@", customSequence.head); // 打印: 1
NSLog(@"%@", customSequence.tail);
// 打印:
<RACArraySequence: 0x7fb239e0c320>{ name = , array = (
2
) }
NSLog(@"%@", customSequence.array);
// 打印:
(
1,
2
)
从这里其实我们可以看出来sequence是怎么实现lazy的, head和tail可以完整表述一个sequence, 而这两者是getter都是通过block来实现, 也就是说, 不用的时候我不调用这个block, 用到了我再调用就是. 所以, 我们可以猜想, eager肯定是不带block的. 通过源码可以知道, eagerSequence是专门有自己的类RACEagerSequence. sequence返回eagerSequence的实现是:
- (RACSequence *)eagerSequence {
return [RACEagerSequence sequenceWithArray:self.array offset:0];
}
正如我们猜想的一样, 没有block, 当然仅仅看这些代码还是不足够说明情况的, 能够真正说明eager和lazy的还要看bind操作, 这里仍然需要一点耐心, 等待一下.
至此我们把sequence里面相关的函数都介绍了, 前面的大多数方法都没有直接分析源码的实现, 只是看了表象, 那是因为最终的调用函数都是bind, 所以为了减少复杂度, 我们先看怎么使用. RACSequence的父类RACStream中则介绍了具体的细节, 里面会根据源码来具体分析.
RACStream
先来看看接口:
+ (instancetype)empty;
+ (instancetype)return:(id)value;
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;
- (instancetype)concat:(RACStream *)stream;
- (instancetype)zipWith:(RACStream *)stream;
- (instancetype)flattenMap:(RACStream * (^)(id value))block;
- (instancetype)flatten;
- (instancetype)map:(id (^)(id value))block;
- (instancetype)mapReplace:(id)object;
- (instancetype)filter:(BOOL (^)(id value))block;
- (instancetype)ignore:(id)value;
- (instancetype)reduceEach:(id (^)())reduceBlock;
- (instancetype)startWith:(id)value;
- (instancetype)skip:(NSUInteger)skipCount;
- (instancetype)take:(NSUInteger)count;
+ (instancetype)zip:(id<NSFastEnumeration>)streams;
+ (instancetype)zip:(id<NSFastEnumeration>)streams reduce:(id (^)())reduceBlock;
+ (instancetype)concat:(id<NSFastEnumeration>)streams;
- (instancetype)scanWithStart:(id)startingValue reduce:(id (^)(id running, id next))reduceBlock;
- (instancetype)scanWithStart:(id)startingValue reduceWithIndex:(id (^)(id running, id next, NSUInteger index))reduceBlock;
- (instancetype)combinePreviousWithStart:(id)start reduce:(id (^)(id previous, id current))reduceBlock;
- (instancetype)takeUntilBlock:(BOOL (^)(id x))predicate;
- (instancetype)takeWhileBlock:(BOOL (^)(id x))predicate;
- (instancetype)skipUntilBlock:(BOOL (^)(id x))predicate;
- (instancetype)skipWhileBlock:(BOOL (^)(id x))predicate;
- (instancetype)distinctUntilChanged;
貌似有点太多了, 这里面我删除了和name相关的, name应该是仅仅作为调试用的.
我们还是一个个分析吧:
从源码中可以这里面大多数接口都是抽象方法, 需要子类来实现的, RACStream中是直接返回了nil的, 例如:
+ (instancetype)empty {
return nil;
}
- (instancetype)bind:(RACStreamBindBlock (^)(void))block {
return nil;
}
+ (instancetype)return:(id)value {
return nil;
}
- (instancetype)concat:(RACStream *)stream {
return nil;
}
- (instancetype)zipWith:(RACStream *)stream {
return nil;
}
所以针对这些方法, 我们需要结合sequence的实现来看:
empty
看过Swift后会知道, nil在Swift中是一个Optional, 而Optional本质上是一个枚举, 所以, 这个empty就和Swift中的nil是一样的. 至于为什么要造一个类似nil的东西, 毕竟往NSArray里面塞nil是会crash的...
// RACSequence.m
+ (instancetype)empty {
return RACEmptySequence.empty;
}
// RACEmptySequence.m
+ (instancetype)empty {
static id singleton;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
singleton = [[self alloc] init];
});
return singleton;
}
empty返回RACEmptySequence的单例, 仅仅代表空而已, 这个类里面其它的方法也都证明它存在的意义:
- (id)head {
return nil;
}
- (RACSequence *)tail {
return nil;
}
- (RACSequence *)bind:(RACStreamBindBlock)bindBlock passingThroughValuesFromSequence:(RACSequence *)passthroughSequence {
return passthroughSequence ?: self;
}
return:
return也比较简单, 把一个元素提升为一个sequence, 源码:
// RACSequence.m
+ (instancetype)return:(id)value {
return [RACUnarySequence return:value];
}
// RACUnarySequence.m
+ (instancetype)return:(id)value {
RACUnarySequence *sequence = [[self alloc] init];
sequence.head = value;
return [sequence setNameWithFormat:@"+return: %@", [value rac_description]];
}
这里引入了一个新类, 虽然看起来会增加复杂度, 但是正如其名, 它就是一个一元的sequence, 没有更多特殊的地方, 除了重载了父类的bind方法, 其余的都很常规, 这个后面再看.
concat:
按顺序先讲bind, 但是这个方法是后面的最终调用, 所以咱们先看看其它方法的表象.
concat正如字面意思, 就是连接2个元素, 所以它的参数也是一个RACStream.
NSArray *letters = @[@"A",@"B",@"C"];
NSArray *concat = [[numbers.rac_sequence concat:letters.rac_sequence] array];
NSLog(@"%@", concat);
// 打印:
(
1,
2,
3,
A,
B,
C
)
zipWith:
Swift中有元组这个概念, 但是ObjC里面则没有, 因此特意引入了进来, 叫RACTuple, 也没有什么特别多好讲的, 看看头文件就知道是什么样的, 本质上就是一个数组而已.
NSArray *zip = [[numbers.rac_sequence zipWith:letters.rac_sequence] array];
NSLog(@"%@", zip);
// 打印:
(
"<RACTuple: 0x7ff3d0616830> (\n 1,\n A\n)",
"<RACTuple: 0x7ff3d06954a0> (\n 2,\n B\n)",
"<RACTuple: 0x7ff3d0603d90> (\n 3,\n C\n)"
)
从RACTuple取出原始值和数组差不多, 看看接口就能明白.
map:与flattenMap:
在Swift中也有类似函数, 只是flattenMap叫flatMap而已, 在Swift中, 这两个函数的区别主要是map过滤nil, 而flatMap会过滤. 之前说了, 这里的nil被empty所取代, 所以可以猜测, map不会过滤掉empty, 而flattenMap会:
NSArray *map = [[numbers.rac_sequence flattenMap:^RACStream *(id value) {
return RACSequence.empty;
}] array];
NSLog(@"%@", map);
// 打印:
(
)
map = [[numbers.rac_sequence map:^id(id value) {
return RACSequence.empty;
}] array];
NSLog(@"%@", map);
// 打印:
(
"<RACEmptySequence: 0x7ff98b225020>{ name = }",
"<RACEmptySequence: 0x7ff98b225020>{ name = }",
"<RACEmptySequence: 0x7ff98b225020>{ name = }"
)
这符合我们的猜测, 但是从头文件的描述和block的返回值我们知道, map和flattenMap差别还是挺大的, 一个返回一个value, 一个返回stream, 所以我们直接从源码入手看看到底有多大差别:
// RACStream.m
- (instancetype)flattenMap:(RACStream * (^)(id value))block {
Class class = self.class;
return [[self bind:^{
return ^(id value, BOOL *stop) {
id stream = block(value) ?: [class empty];
NSCAssert([stream isKindOfClass:RACStream.class], @"Value returned from -flattenMap: is not a stream: %@", stream);
return stream;
};
}] setNameWithFormat:@"[%@] -flattenMap:", self.name];
}
- (instancetype)map:(id (^)(id value))block {
NSCParameterAssert(block != nil);
Class class = self.class;
return [[self flattenMap:^(id value) {
return [class return:block(value)];
}] setNameWithFormat:@"[%@] -map:", self.name];
}
可以看到, map调用了flattenMap来实现, 直接把每一个value都包装成一个一元sequence交给flattenMap来处理, 而flattenMap又调用了bind, 所以我们借此机会直接看bind的实现吧.
bind
正式看bind之前, 我们先来讲讲前面提到的eagerSequence和lazySequence吧, 因为bind里面用到因为要懒加载的类. 我们先直接看eager和lazy的对比:
RACSequence *sequence = [numbers.rac_sequence map:^id(id value) {
NSLog(@"caculating 1...");
return [value description];
}];
RACSequence *sequence2 = [numbers.rac_sequence.eagerSequence map:^id(id value) {
NSLog(@"caculating 2...");
return [value description];
}];
[sequence array];
// 打印出:
calculating 2...
calculating 2...
calculating 2...
calculating 1...
calculating 1...
calculating 1...
顺序是反的, 先2再1, 所以这也印证了lazy的sequence是在使用的时候再计算的观点.
我们知道这些操作最终都是通过调用bind来实现的, 所以eager和lazy最大的区别也就是bind操作, 我们先看RACEagerSequence对bind的实现:
- (instancetype)bind:(RACStreamBindBlock (^)(void))block {
NSCParameterAssert(block != nil);
RACStreamBindBlock bindBlock = block();
NSArray *currentArray = self.array;
NSMutableArray *resultArray = [NSMutableArray arrayWithCapacity:currentArray.count];
for (id value in currentArray) {
BOOL stop = NO;
RACSequence *boundValue = (id)bindBlock(value, &stop);
// 直接返回nil的话会break掉
if (boundValue == nil) break;
for (id x in boundValue) {
[resultArray addObject:x];
}
if (stop) break;
}
return [[self.class sequenceWithArray:resultArray offset:0] setNameWithFormat:@"[%@] -bind:", self.name];
}
非常简单直观, 拿到当前的array进行遍历, 然后对每一个元素进行bindBlock操作, bindBlock返回的是一个sequence, 所以再对里面进行遍历(现在用到了前面sequence支持的快速遍历), 把结果加进结果集中, 最后再返回一个sequence, 注意还是这里是self.class, 所以返回的依然是eager的. 所以可以看到, eagerSequence在bind的时候就会把所有的东西都计算好, 这也是和lazySequence最大的区别来源.
所以从上面的代码我们可以看出来bind:操作具体所做的事情是什么了, 也明白了为什么map操作一定要把value包装成一个一元sequence了.
接下来, 再来看看lazySequence的核心(细节先隐藏掉):
// 外部都是直接调用bind:, 这里多加了一个参数, 是为了递归
- (instancetype)bind:(RACStreamBindBlock)bindBlock passingThroughValuesFromSequence:(RACSequence *)passthroughSequence {
__block RACSequence *valuesSeq = self;
__block RACSequence *current = passthroughSequence;
__block BOOL stop = NO;
RACSequence *sequence = [RACDynamicSequence sequenceWithLazyDependency:^ id {
.....
} headBlock:^(id _) {
......
} tailBlock:^ id (id _) {
......
}];
sequence.name = self.name;
return sequence;
}
首先可以看到的是, 我们对sequence调用bind: 最终返回的是一个dynamicSequence, 那我们继续跟踪到dynamicSequence里面:
+ (RACSequence *)sequenceWithLazyDependency:(id (^)(void))dependencyBlock headBlock:(id (^)(id dependency))headBlock tailBlock:(RACSequence *(^)(id dependency))tailBlock {
NSCParameterAssert(dependencyBlock != nil);
NSCParameterAssert(headBlock != nil);
RACDynamicSequence *seq = [[RACDynamicSequence alloc] init];
seq.headBlock = [headBlock copy];
seq.tailBlock = [tailBlock copy];
seq.dependencyBlock = [dependencyBlock copy];
seq.hasDependency = YES;
return seq;
}
看起来很普通的一个新建一个sequence实例的样子, 回想到我们之前说的, 只要有了head和tail, 就能完整描述一个sequence, 我们来看看实现(head和tail是一样的, 所以以head为例):
- (id)head {
//这里很多操作都需要原子性来保证线程安全
@synchronized (self) {
// 先拿到headBlock, 有就继续, 没有就返回
id untypedHeadBlock = self.headBlock;
if (untypedHeadBlock == nil) return _head;
// 判断有没有依赖, 有就先执行依赖, 没有就直接执行headBlock算值
if (self.hasDependency) {
if (self.dependencyBlock != nil) {
// 拿到依赖值之后就置空, 这样就不会多次执行,
_dependency = self.dependencyBlock();
self.dependencyBlock = nil;
}
// 用headBlock算出值
id (^headBlock)(id) = untypedHeadBlock;
_head = headBlock(_dependency);
} else {
id (^headBlock)(void) = untypedHeadBlock;
_head = headBlock();
}
// 拿到head后就置空, 下次就直接返回了
self.headBlock = nil;
return _head;
}
}
这里看到了lazy的实现细节, 但是又引入了一个新的细节, 就是dependency, 我们再来看看. dependencyBlock算出的值在headBlock中使用了(tailBlock中也是), 然后我们看到之前的bind函数中, 直接无视了这个值, 所以我们基本上可以猜测, 在sequence这个场景中, dependency的代码只是在计算head或tail的时候的一些前置操作, tail和head本身并不一定关心这个值, 因此我们回到bind函数来看看dependency的代码:
[RACDynamicSequence sequenceWithLazyDependency:^ id {
// 这里用while循环是为了当bind操作返回empty(空数组的sequence也是empty, 回头看看)的时候要继续往下走, 直到没有空为止
// 这么做的原因要牵涉到快速遍历的实现代码, 这里先按下不表
while (current.head == nil) {
if (stop) return nil;
// 取出自身的head
id value = valuesSeq.head;
// 如果为nil, 说明已经递归完毕了
if (value == nil) {
stop = YES;
return nil;
}
// 拿到bind操作处理的sequence
current = (id)bindBlock(value, &stop);
// 和eager一样, 拿到nil就直接break
if (current == nil) {
stop = YES;
return nil;
}
// 还记得tail就是把返回当前sequence的offset加1的新sequence吗?
valuesSeq = valuesSeq.tail;
}
NSCAssert([current isKindOfClass:RACSequence.class], @"-bind: block returned an object that is not a sequence: %@", current);
return nil;
} ...];
可以看出, dependency的操作本质上是为了保证快速遍历能够把所有的元素都遍历到, 因为快速遍历的本质就是一直nextObject, 直到接收到nil:
// RACSequence.m的快速遍历方法:
...
__autoreleasing id obj = seq.head;
if (obj == nil) {
complete();
break;
}
...
最后再来看dynamicSequence的headBlock和tailBlock就比较简单了(不要忘记他们俩啊!!!)
[RACDynamicSequence sequenceWithLazyDependency:^ id {
...
} headBlock:^(id _) {
// current指向的是自己, 返回自己的head
return current.head;
} tailBlock:^ id (id _) {
if (stop) return nil;
// 一旦遍历到的不是empty就需要递归对tail中的元素进行bind操作
return [valuesSeq bind:bindBlock passingThroughValuesFromSequence:current.tail];
}];
至此bind操作算是基本说清楚了, 但是, 貌似还有个问题, 还是没有说明白map和flattenMap的区别, 其实区别一开始就讲了, map对empty进行了一层包装, 而flattenMap没有, 所以, 包装了的, head拿出来是empty, 这个不为nil, 所以自然可以继续快速遍历, 然后一个个加进结果集, 而没有包装的, head拿出来是nil, 所以直接就GG思密达了.
flattern
有了flattenMap还有一个flatten, 先看看代码:
- (instancetype)flatten {
__weak RACStream *stream __attribute__((unused)) = self;
return [self flattenMap:^(id value) {
return value;
}] ;
}
可以看到, 直接把自己内部的元素给flattenMap了, 所以这里就要求自己内部的元素必须是RACStream类型的, 实验代码如下:
NSArray *map = [[@[numbers.rac_sequence, letters.rac_sequence].rac_sequence flatten] array];
NSLog(@"%@", map);
// 打印出:
(
1,
2,
3,
A,
B,
C
)
怎么和concat是一样的呢? 来看看concat的代码:
- (instancetype)concat:(RACStream *)stream {
NSCParameterAssert(stream != nil);
return [[[RACArraySequence sequenceWithArray:@[ self, stream ] offset:0]
flatten]
setNameWithFormat:@"[%@] -concat: %@", self.name, stream];
}
直接调用的flatten, 不过需要注意的是concat是Sequence的方法, RACStream是直接返回nil的.
mapReplace:
很简单一个操作, 把全部元素都替换成传入的object. 不演示了.
filter:
前面说了any:和all:就是调用它的, 来看看实现:
- (instancetype)filter:(BOOL (^)(id value))block {
NSCParameterAssert(block != nil);
Class class = self.class;
return [[self flattenMap:^ id (id value) {
if (block(value)) {
return [class return:value];
} else {
return class.empty;
}
}] setNameWithFormat:@"[%@] -filter:", self.name];
}
满足条件直接添加, 不满足加为empty, 再利用flattenMap过滤掉empty.
ignore:
顾名思义, 忽略掉某个值, 最终肯定也是调用filter:操作, 如下代码所示:
- (instancetype)ignore:(id)value {
return [[self filter:^ BOOL (id innerValue) {
return innerValue != value && ![innerValue isEqual:value];
}] setNameWithFormat:@"[%@] -ignore: %@", self.name, [value rac_description]];
}
可以传入nil, 但是nil与empty不等, 所以不会排除掉empty.
reduceEach:
专门为Tuple写的一个遍历方法:
NSArray *tuples = @[RACTuplePack(@1,@"A"),RACTuplePack(@2,@"B"),RACTuplePack(@3,@"C")];
NSArray *mapRepleace = [[tuples.rac_sequence reduceEach:^id(id value, id value2){
NSLog(@"value1: %@, value2: %@", value, value2);
return @[value, value2];
}] array];
// 打印出:
value1: 1, value2: A
value1: 2, value2: B
value1: 3, value2: C
可能对Tuple用的多的话会用到这个方法吧, 例如取出每一个Tuple中的某个值
startWith:
看起来好像是截断一下, 从某个值开始, 但是这个start与reduce的start一样, 都是给个初始值, 所以是从前面新增一个值, 如果为nil会直接跳过:
skip:与take:
一个是跳过几个值, 一个是取几个值, 对应的还有skipUntilBlock:, skipWhileBlock, takeUntilBlock, takeWhileBlock, 都是一个意思.
+zip:与+concat:
与前面的zip和concat是一样的效果, 只不过这个是类方法.
需要注意的是zip的实例方法和类方法的实现不太一样, 实例方法是创建sequence, 是属于sequence的方法:
- (instancetype)zipWith:(RACSequence *)sequence {
NSCParameterAssert(sequence != nil);
return [[RACSequence
sequenceWithHeadBlock:^ id {
if (self.head == nil || sequence.head == nil) return nil;
return RACTuplePack(self.head, sequence.head);
} tailBlock:^ id {
if (self.tail == nil || [[RACSequence empty] isEqual:self.tail]) return nil;
if (sequence.tail == nil || [[RACSequence empty] isEqual:sequence.tail]) return nil;
return [self.tail zipWith:sequence.tail];
}]
setNameWithFormat:@"[%@] -zipWith: %@", self.name, sequence];
}
而类方法的zip是RACStream的方法:
+ (instancetype)zip:(id<NSFastEnumeration>)streams {
return [[self join:streams block:^(RACStream *left, RACStream *right) {
return [left zipWith:right];
}] setNameWithFormat:@"+zip: %@", streams];
}
+ (instancetype)join:(id<NSFastEnumeration>)streams block:(RACStream * (^)(id, id))block {
RACStream *current = nil;
for (RACStream *stream in streams) {
if (current == nil) {
// 这里先把第一个sequence的值打包
current = [stream map:^(id x) {
return RACTuplePack(x);
}];
continue;
}
// 再把后面的值反复打包
current = block(current, stream);
}
if (current == nil) return [self empty];
return [current map:^(RACTuple *xs) {
NSMutableArray *values = [[NSMutableArray alloc] init];
// 开始拆包, 最后一个是不需要拆的, 可以直接add, 后面xs=xs.first可以拿出前面打包的值, 然后继续加最后一个, 再拿, 循环拿出全部包的最后一个值, 然后就遍历完了
while (xs != nil) {
[values insertObject:xs.last ?: RACTupleNil.tupleNil atIndex:0];
xs = (xs.count > 1 ? xs.first : nil);
}
return [RACTuple tupleWithObjectsFromArray:values];
}];
}
scanWithStart:recude:与scanWithStart:recudeWithIndex:
和reduce看起来很像, 但是实际上却大有不同, 先看看表象:
NSArray *scan = [[numbers.rac_sequence scanWithStart:@"start" reduce:^id(id running, id next) {
return [NSString stringWithFormat:@"%@|%@", running, next];
}] array];
NSLog(@"%@", scan);
// 打印:
(
"start|1",
"start|1|2",
"start|1|2|3"
)
之前的reduce仅仅打印出"start|1|2|3", 具体还是用bind:操作:
- (instancetype)scanWithStart:(id)startingValue reduceWithIndex:(id (^)(id, id, NSUInteger))reduceBlock {
NSCParameterAssert(reduceBlock != nil);
Class class = self.class;
return [[self bind:^{
__block id running = startingValue;
__block NSUInteger index = 0;
return ^(id value, BOOL *stop) {
running = reduceBlock(running, value, index++);
return [class return:running];
};
}] setNameWithFormat:@"[%@] -scanWithStart: %@ reduceWithIndex:", self.name, [startingValue rac_description]];
}
所以返回的是一个sequence, 再回头看看reduce的实现:
- (id)foldLeftWithStart:(id)start reduce:(id (^)(id, id))reduce {
NSCParameterAssert(reduce != NULL);
if (self.head == nil) return start;
for (id value in self) {
start = reduce(start, value);
}
return start;
}
和我们之前的数学表达式是一样的, 循环执行, 最后算出结果, 只有一个值.
combinePreviousWithStart:reduce:
和scan不一样, scan是一直累计, 而combine则是两两结合:
NSArray *comibine = [[numbers.rac_sequence combinePreviousWithStart:@"start" reduce:^id(id previous, id current) {
return [NSString stringWithFormat:@"%@|%@", previous, current];
}] array];
NSLog(@"%@", comibine);
// 打印出:
(
"start|1",
"1|2",
"2|3"
)
来看看实现好了:
- (instancetype)combinePreviousWithStart:(id)start reduce:(id (^)(id previous, id next))reduceBlock {
NSCParameterAssert(reduceBlock != NULL);
return [[[self
scanWithStart:RACTuplePack(start)
reduce:^(RACTuple *previousTuple, id next) {
id value = reduceBlock(previousTuple[0], next);
return RACTuplePack(next, value);
}]
map:^(RACTuple *tuple) {
return tuple[1];
}]
setNameWithFormat:@"[%@] -combinePreviousWithStart: %@ reduce:", self.name, [start rac_description]];
}
直接调用的scan, 利用Tuple打包, 然后map出来后面的值, 也就是我们处理过的value, 还是比较巧妙的.
distinctUntilChanged
一个有意思的函数, 如果与前面的对象重复了, 那就过滤掉
NSArray *distinctUntilChanged = [[@[@1,@2,@3,@1,@1].rac_sequence distinctUntilChanged] array];
NSLog(@"%@", distinctUntilChanged);
// 打印出:
(
1,
2,
3,
1
)
至此, NSArray的东西(其实也包含了很多其它的Category会用到的东西)都已经讲完了, 介绍了很多的操作, 这些操作还是需要去记忆他们之间的区别的, 我相信记下之后, 总会有其用武之地的.
另外值得注意的是, 这里的操作所产生的对象都是中间变量, 这和函数式编程思想是一致的, 只关注输入和输出, 不关注中间的引用.
再说点废话
这些东西都是这段时间看代码的一些新的, 如果里面有错或者还是不够清晰的地方希望能够评论给我, 我会继续修改. 祝大家学习愉快.
另外, 这篇文章花了我很多前期学习和一下午的写撰写时间, 如果需要转载请注明出处, 谢谢.
附录
- ReactiveCocoa gitHub地址: 里面的文档在最开始接触还是值得看看的, 如果有机会可以翻译一下
- RAC Marbles: 一个演示各个操作效果的小动画