ReactiveCocoa学习笔记一--NSArray溯源

废话说在前面

不知道有多少人和我一样, 觉得现在学习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会用到的东西)都已经讲完了, 介绍了很多的操作, 这些操作还是需要去记忆他们之间的区别的, 我相信记下之后, 总会有其用武之地的.

另外值得注意的是, 这里的操作所产生的对象都是中间变量, 这和函数式编程思想是一致的, 只关注输入和输出, 不关注中间的引用.

再说点废话

这些东西都是这段时间看代码的一些新的, 如果里面有错或者还是不够清晰的地方希望能够评论给我, 我会继续修改. 祝大家学习愉快.
另外, 这篇文章花了我很多前期学习和一下午的写撰写时间, 如果需要转载请注明出处, 谢谢.

附录

  1. ReactiveCocoa gitHub地址: 里面的文档在最开始接触还是值得看看的, 如果有机会可以翻译一下
  2. RAC Marbles: 一个演示各个操作效果的小动画
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容