ReactiveCocoa Documents 翻译(基于版本V2.5)

前端之家收集整理的这篇文章主要介绍了ReactiveCocoa Documents 翻译(基于版本V2.5)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

1. 基本操作(Basic Operators)

描述 ReactiveCocoa 最常用的一些操作以及使用范例。 主要是如何运用 序列(sequences) 和 信号(signals) 的流操作。

用信号实现副作用(Performing side effects with signals)

@H_404_8@
  • 订阅(Subscription)
  • 依赖注入(Injecting effects)
  • 流的传输(Transforming streams)

    @H_404_8@
  • 映射(Mapping)
  • 过滤(Filtering)
  • 流的结合(Combining streams)

    @H_404_8@
  • 串联(Concatenating)
  • 压缩(Flattening)
  • 映射和压缩(Mapping and flattening)
  • 信号的结合(Combining signals)

    @H_404_8@
  • 排序(Sequencing)
  • 合并(Merging)
  • 结合最新值(Combining latest values)
  • 切换(Switching)
  • 1.1 用信号实现副作用(Performing side effects with signals)

    译者注:什么是冷信号,什么是热信号?

    • 冷信号:被订阅的时候才激活。信号默认是冷类型的。
    • 热信号:返回给调用者的时候已经被激活。

    译者注:什么是push-driven? 什么是pull-driven?

    • push-driven:在创建信号的时候,信号不会被立即赋值,之后才会被赋值(例如网络请求回来的结果或者是任意的用户输入的结果)。
    • pull-driven:在创建信号的时候,序列中的值就会被确定下来,我们可以从流中一个个的查询值。

    大多数信号(signals)初始化是冷(cold)信号,这意味着它们在被订阅(Subscription)之前不会做任何工作。
    订阅之后,信号或者它的订阅者能够完成边界效应,比如输出日志到控制台,做一个网络请求,修改用户界面等。
    副作用也可以被注入到一个信号,这样的副作用不会立刻完成,但是会被稍后的每一个订阅者触发副作用。

    副作用: 当调用函数时,除了返回函数值之外,还对主调用函数产生附加影响,这就叫函数的副作用。 ReactiveCocoa 的函数参数是 In/Out 作用的参数,即函数可能改变参数里面的的内容,把一些信息通过输入参数,夹带到外界。 这种情况严格来说也是副作用,是非纯函数。我们所讨论的函数式反应式编程中的函数式编程属于非纯函数,它是有副作用的。

    1.1.1 订阅(Subscription)

    -subscribe...方法给你机会访问信号中的当前或者将来的值:

    RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
    
    // 输出: A B C D E F G H I
    [letters subscribeNext:^(NSString *x) {
        NSLog(@"%@",x);
    }];

    对于冷信号来说,副作用会在每一次订阅时发生:

    __block unsigned subscriptions = 0;
    
    RACSignal *loggingSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
        subscriptions++;
        [subscriber sendCompleted];
        return nil;
    }];
    
    // 输出:
    // subscription 1
    [loggingSignal subscribeCompleted:^{
        NSLog(@"subscription %u",subscriptions);
    }];
    
    // 输出:
    // subscription 2
    [loggingSignal subscribeCompleted:^{
        NSLog(@"subscription %u",subscriptions);
    }];

    行为可以被connection(后面会讲到connection) 改变.

    1.1.2 注入影响(Injecting effects)

    do...方法给信号添加副作用而不需要实际订阅信号:

    __block unsigned subscriptions = 0;
    
    RACSignal *loggingSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
        subscriptions++;
        [subscriber sendCompleted];
        return nil;
    }];
    
    // 没有任何输出
    loggingSignal = [loggingSignal doCompleted:^{
        NSLog(@"about to complete subscription %u",subscriptions);
    }];
    
    // 输出:
    // about to complete subscription 1
    // subscription 1
    [loggingSignal subscribeCompleted:^{
        NSLog(@"subscription %u",subscriptions);
    }];

    1.2 流的转换(Transforming streams)

    下面的操作将流转换为一个新的流。

    1.2.1 映射(Mapping)

    -map...方法被用来传递一个值给流,然后用该值创建一个新的流。

    RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
    
    // Contains: AA BB CC DD EE FF GG HH II
    RACSequence *mapped = [letters map:^(NSString *value) {
        return [value stringByAppendingString:value];
    }];

    1.2.2 过滤(Filtering)

    -filter...方法用一个 block 测试每一个值,使得结果流中只包含测试通过的内容

    RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
    
    // Contains: 2 4 6 8
    RACSequence *filtered = [numbers filter:^ BOOL (NSString *value) {
        return (value.intValue % 2) == 0;
    }];

    1.3 流的合并(Combining streams)

    下面的操作合并多个流到一个单一的新流。

    1.3.1 串联(Concatenating)

    -concat:方法追加一个流的值到另外一个流后面。

    RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
    RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
    
    // Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
    RACSequence *concatenated = [letters concat:numbers];

    1.3.2 扁平(Flattening,这个真不知道怎么翻译)

    -flatten操作适用于基于流的流,合并他们的值到一个新的流中。

    序列是串联(concatenated)的

    RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
    RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
    RACSequence *sequenceOfSequences = @[ letters,numbers ].rac_sequence;
    
    // Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
    RACSequence *flattened = [sequenceOfSequences flatten];

    信号是合并(merged)的:

    RACSubject *letters = [RACSubject subject];
    RACSubject *numbers = [RACSubject subject];
    RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
        [subscriber sendNext:letters];
        [subscriber sendNext:numbers];
        [subscriber sendCompleted];
        return nil;
    }];
    
    RACSignal *flattened = [signalOfSignals flatten];
    
    // Outputs: A 1 B C 2
    [flattened subscribeNext:^(NSString *x) {
        NSLog(@"%@",x);
    }];
    
    [letters sendNext:@"A"];
    [numbers sendNext:@"1"];
    [letters sendNext:@"B"];
    [letters sendNext:@"C"];
    [numbers sendNext:@"2"];

    1.3.3 映射和扁平(Mapping and flattening)

    flattening本身并不有趣,但弄懂它的-flattenMap方法是如何工作的很重要。

    -flattenMap:用来传递流的每一个值到一个新的流。然后,所有返回的流会被扁平到一个新的流。 也就是说,它相当于-flatten操作后再-map:操作。

    可用来扩展或编辑序列:

    RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
    
    // Contains: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
    RACSequence *extended = [numbers flattenMap:^(NSString *num) {
        return @[ num,num ].rac_sequence;
    }];
    
    // Contains: 1_ 3_ 5_ 7_ 9_
    RACSequence *edited = [numbers flattenMap:^(NSString *num) {
        if (num.intValue % 2 == 0) {
            return [RACSequence empty];
        } else {
            NSString *newNum = [num stringByAppendingString:@"_"];
            return [RACSequence return:newNum]; 
        }
    }];

    或者创建多个信号自动合并:

    RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
    
    [[letters
        flattenMap:^(NSString *letter) {
            return [database saveEntriesForLetter:letter];
        }]
        subscribeCompleted:^{
            NSLog(@"All database entries saved successfully.");
        }];

    1.4 信号组合(Combining signals)

    下面的操作组合多个信号到一个新信号。

    1.4.1 序列化(Sequencing)

    -then:启动原始的信号,等待它完成,然后只转发新信号中的值。

    RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
    
    // The new signal only contains: 1 2 3 4 5 6 7 8 9
    //
    // But when subscribed to,it also outputs: A B C D E F G H I
    RACSignal *sequenced = [[letters
        doNext:^(NSString *letter) {
            NSLog(@"%@",letter);
        }]
        then:^{
            return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;
        }];

    这在某些情况下很有用,比如执行一个信号的所有副作用,然后开始另外一个信号,并且只返回第二个信号的值。

    1.4.2 合并(Merging)

    +merge:方法会尽可能快的从很多信号中转发值到一个流中,在值到达的时候。

    RACSubject *letters = [RACSubject subject];
    RACSubject *numbers = [RACSubject subject];
    RACSignal *merged = [RACSignal merge:@[ letters,numbers ]];
    
    // Outputs: A 1 B C 2
    [merged subscribeNext:^(NSString *x) {
        NSLog(@"%@",x);
    }];
    
    [letters sendNext:@"A"];
    [numbers sendNext:@"1"];
    [letters sendNext:@"B"];
    [letters sendNext:@"C"];
    [numbers sendNext:@"2"];

    1.4.3 组合最新值(Combining latest values)

    + combineLatest:+combineLatest:reduce:方法会观察多个信号的改变,然后从所有信号发送最新的值:

    RACSubject *letters = [RACSubject subject];
    RACSubject *numbers = [RACSubject subject];
    RACSignal *combined = [RACSignal
        combineLatest:@[ letters,numbers ]
        reduce:^(NSString *letter,NSString *number) {
            return [letter stringByAppendingString:number];
        }];
    
    // Outputs: B1 B2 C2 C3
    [combined subscribeNext:^(id x) {
        NSLog(@"%@",x);
    }];
    
    [letters sendNext:@"A"];
    [letters sendNext:@"B"];
    [numbers sendNext:@"1"];
    [numbers sendNext:@"2"];
    [letters sendNext:@"C"];
    [numbers sendNext:@"3"];

    注意结合信号会只发送第一个值,当所有输入被发送至少一个的时候。上面的例子中,@"A"不会被转发因为number没有被发送一个值。

    1.4.4 切换(Switching)

    -switchToLatest操作适用于基于信号的信号,并且总是从最新信号传递值。

    RACSubject *letters = [RACSubject subject];
    RACSubject *numbers = [RACSubject subject];
    RACSubject *signalOfSignals = [RACSubject subject];
    
    RACSignal *switched = [signalOfSignals switchToLatest];
    
    // Outputs: A B 1 D
    [switched subscribeNext:^(NSString *x) {
        NSLog(@"%@",x);
    }];
    
    [signalOfSignals sendNext:letters];
    [letters sendNext:@"A"];
    [letters sendNext:@"B"];
    
    [signalOfSignals sendNext:numbers];
    [letters sendNext:@"C"];
    [numbers sendNext:@"1"];
    
    [signalOfSignals sendNext:letters];
    [numbers sendNext:@"2"];
    [letters sendNext:@"D"];

    2 设计指南(Design Guidelines)

    本文档包含如何在工程中使用 ReactiveCocoa 的设计指南。本章的内容重度参考了Rx Design Guidelines

    本文假设读者熟悉 ReactiveCocoa 的基本功能Framework Overview是开始了解 RAC 的更好的资源。

    RACSequence的约定

    @H_404_8@
  • 运算默认是懒执行模式。
  • 运算会阻塞调用者。
  • 副作用只发生一次。
  • RACSignal的约定

    @H_404_8@
  • 信号事件是串行的。
  • 订阅都是发生在调度的时候。
  • 错误会被立即传送出来。
  • 副作用发生在每次订阅
  • 订阅自动部署完成和错误
  • Disposal取消正在进行的工作并且清除资源。
  • 最佳实践

    @H_404_8@
  • 为返回信号的方法属性使用描述性的声明。
  • 始终缩进流操作。
  • 流的所有值都使用相同的类型。
  • 不要retain流过长时间。
  • 只处理需要数量的流。
  • 分发信号事件到一个已知的调度。
  • 较少的场合需要切换调度者。
  • 明确信号的副作用。
  • 通过multicasting共享信号的副作用。
  • 通过指定的流的名字来调试。
  • 避免明确的订阅(subscriptions)和释放(disposal)。
  • 尽可能避免使用subjects。
  • 完成新operators

    @H_404_8@
  • 优先使用基于 RACStream 方法
  • 尽可能组合已存在的operators。
  • 避免引入并发。
  • 在dispasable中取消任务和清理所有资源。
  • 在operator中不要阻塞。
  • 深度递归要避免栈溢出。
  • 2.1 RACSequence的约定(The RACSequence contract)

    RACSequencepull-driven流。序列行为类似内置集合,但有些不一样的地方。

    2.1.1 (运算默认是懒执行模式)Evaluation occurs lazily by default

    序列运算默认是懒执行模式,如下面的序列:

    NSArray *strings = @[ @"A",@"B",@"C" ];
    RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
        return [str stringByAppendingString:@"_"];
    }];

    没有字符串会被实际追加直到序列真正需要的时候。 访问sequence.head会完成A_的拼接,访问sequence.tail.head会完成B_的拼接,等等。

    这通常能避免不必要的工作(因为不需要的值不会被计算),但意味着序列是要处理的。

    一旦被计算,序列中的值就被存储不会被重新计算。访问sequence.head多次只会做一次字符串的拼接工作。

    如果懒式运算模式不可取 - 例如,因为内存有限的时候,较少使用内存更重要 - eagerSequence 属性可能被强制转为饥渴模式。

    2.1.2 运算会阻塞调用者(Evaluation blocks the caller)

    不管序列是懒模式还是饥渴模式,运算序列的任何部分都会阻塞调用者线程直到任务完成。阻塞是必须的因为值必须从序列中同步返回。

    如果运算序列序列的代价大到可能阻塞线程很明显的时间,考虑用-signalWithScheduler:创建一个信号然后用它来替代序列。

    2.1.3 副作用只发生一次(Side effects occur only once)

    当传递给序列操作的 block 引发了副作用,要明白副作用对每个值只会发生一次,就是在值被运算的时候。

    NSArray *strings = @[ @"A",@"C" ];
    RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
        NSLog(@"%@",str);
        return [str stringByAppendingString:@"_"];
    }];
    
    // Logs "A" during this call.
    NSString *concatA = sequence.head;
    
    // Logs "B" during this call.
    NSString *concatB = sequence.tail.head;
    
    // Does not log anything.
    NSString *concatB2 = sequence.tail.head;
    
    RACSequence *derivedSequence = [sequence map:^(NSString *str) {
        return [@"_" stringByAppendingString:str];
    }];
    
    // Still does not log anything,because "B_" was already evaluated,and the log
    // statement associated with it will never be re-executed.
    NSString *concatB3 = derivedSequence.tail.head;

    2.2 RACSignal 约定(The RACSignal contract)

    RACSignalpush-driven流,专注于通过subscriptions分发异步事件。 关于信号和订阅的更多内容,参见Framework Overview

    2.2.1 信号事件是串行的(Signal events are serialized)

    信号可以在任何线程中分发事件。连续的事件甚至被允许分发到不同的线程或者调度者,除非显示的指定了分发到特定的调度者。

    然而,RAC 不会有两个信号并发到达。一个事件被处理时,不会有另外的事件被分发。其他事件的发送会被强制等待直到当前事件被处理完成。

    特别注意,这意味着传递给-subscribeNext:error:competed:的 block 之间不需要考虑同步,因为他们永远不会被同时调用

    2.2.2 订阅总会在调度的时候发生(Subscription will always occur on a scheduler)

    要保证+createSingnal-subscribe:的行为一致,每一个RACSignal必须确保在合法的调度者上订阅

    如果的订阅者的线程已经有一个+currentScheduler,调度会立刻发生;否则,会在后台调度的时候立刻发生。 注意主线程总是与—mainThreadScheduler关联,所以主线程的订阅总是立刻发生。

    参见文档-subscribe:获取更多信息。

    2.2.3 错误会立即传送出来(Errors are propagated immediately)

    在 RAC中,error事件有特别的语义。当错误被发送给信号,会立即转发给所有依赖的信号,引发整个依赖链的终止。

    Operators的主要目的是改变错误处理行为,但像-catch:,-catchTo:,或者-materialize明显是不符合此规则的。

    2.2.4 副作用发生在每次订阅时(Side effects occur for each subscription)

    RACSignal的每一个新的订阅都会触发信号的副作用。这意味着任何副作用发生的次数和信号订阅次数一样多。

    考虑如下代码

    __block int aNumber = 0;
    
    // Signal that will have the side effect of incrementing `aNumber` block 
    // variable for each subscription before sending it.
    RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
        aNumber++;
        [subscriber sendNext:@(aNumber)];
        [subscriber sendCompleted];
        return nil;
    }];
    
    // This will print "subscriber one: 1"
    [aSignal subscribeNext:^(id x) {
        NSLog(@"subscriber one: %@",x);
    }];
    
    // This will print "subscriber two: 2"
    [aSignal subscribeNext:^(id x) {
        NSLog(@"subscriber two: %@",x);
    }];

    副作用会在每次订阅的时候重复发生。同样适用于streamsignal操作。

    __block int missilesToLaunch = 0;
    
    // Signal that will have the side effect of changing `missilesToLaunch` on
    // subscription.
    RACSignal *processedSignal = [[RACSignal
        return:@"missiles"]
        map:^(id x) {
            missilesToLaunch++;
            return [NSString stringWithFormat:@"will launch %d %@",missilesToLaunch,x];
        }];
    
    // This will print "First will launch 1 missiles"
    [processedSignal subscribeNext:^(id x) {
        NSLog(@"First %@",x);
    }];
    
    // This will print "Second will launch 2 missiles"
    [processedSignal subscribeNext:^(id x) {
        NSLog(@"Second %@",x);
    }];

    要阻止上述行为,在多次订阅一个信号时只执行它的副作用一次,可以用信号的多播功能multicasted

    2.2.5 订阅会在完成和错误的时候自动释放(Subscriptions are automatically disposed upon completion or error)

    当一个subscriber被发送给completed或者error事件,相关的订阅自动被释放。这种行为通常无需手动去配置订阅

    参见文档Memory Management获取signal的更多信息。

    2.2.6 Disposal取消正在进行的工作和清理资源(Disposal cancels in-progress work and cleans up resources)

    订阅被释放的时候,不管手动或自动,任何正在处理或与订阅相关的工作会尽快被取消,订阅相关的资源会被释放。

    2.3 Best practices

    下面的建议有助于保证基于 RAC 的代码可预测,可理解和高效。

    然而,仅仅只是指导。判断是否遵循了建议的标准是下面的代码片段。

    2.3.1 为返回信号的属性方法使用描述性的声明(Use descriptive declarations for methods and properties that return a signal)

    方法属性如果返回RACSignal类型,会很难看懂信号的语义。

    有三个关键问题必须在声明中表达清楚:

    @H_404_8@
  • 信号是(返回给调用者的时候已经被激活)信号还是(被订阅的时候才激活)信号。
  • 信号包含0个,1个,还是多个值?
  • 信号是否有副作用?
  • 没有副作用的热信号应该典型的用属性来代替方法。使用属性意味着在订阅信号的时间之前不需要初始化,并且额外的订阅不会改变语义。 信号属性应该以事件命名(例如textChanged)。

    没有副作用的冷信号应该从名词命名的方法中返回(例如:-currentText)。 这样的方法声明意味着信号不会被该方法保留,暗示着在订阅的时候任务已经完成了。 如果信号发送多个值,名词应该用复数(例如:-currentModels)。

    带副作用的信号应该被动词命名的方法返回(例如:-logIn)。 动词意味着该方法不是幂等的(幂等:意味着多次执行操作的结果和第一次执行的相同。应该就是函数的可重入性), 调用者必须小心只在副作用需要的时候才调用它。如果信号会发送一个或多个值,应该包含一个期望的名词 (例如:-loadConfigration-fecthLatestEvents)。

    2.3.2 始终缩进流操作(Indent stream operations consistently)

    如果没有合适的格式化,流代码和容易变得密集和混乱。使用缩进能够清晰的看出来链式流操作的开始和结束。

    调用流的简单的方法时,不需要要额外的缩进。:

    RACStream *result = [stream startWith:@0];
    
    RACStream *result2 = [stream map:^(NSNumber *value) {
        return @(value.integerValue + 1);
    }];

    如果传输同一个流多次,确保每一个步骤都是对齐的。 复杂的操作比如+zip:reduce:+combineLatest:reduce:可以拆分成多行提高可读性。

    RACStream *result = [[[RACStream
        zip:@[ firstStream,secondStream ]
        reduce:^(NSNumber *first,NSNumber *second) {
            return @(first.integerValue + second.integerValue);
        }]
        filter:^ BOOL (NSNumber *value) {
            return value.integerValue >= 0;
        }]
        map:^(NSNumber *value) {
            return @(value.integerValue + 1);
        }];

    当然,带block参数的嵌套的流应该跟block一起自然缩进:

    [[signal
        then:^{
            @strongify(self);
    
            return [[self
                doSomethingElse]
                catch:^(NSError *error) {
                    @strongify(self);
                    [self presentError:error];
    
                    return [RACSignal empty];
                }];
        }]
        subscribeCompleted:^{
            NSLog(@"All done.");
        }];

    2.3.3 对流的所有的值使用相同的类型(Use the same type for all the values of a stream)

    RACStream包括它的扩展,RACSignalRACSequence)允许流由异质对象(即对象的类型不一致)组成,就像 Cocoa 集合那样。
    然而,在流中使用不同的类型使得操作复杂,还会给流的使用者带来额外的负担,使用者应该只关心如何调用支持方法

    任何可能的时候,流都应该只包含相同类型的对象。

    2.3.4 避免长时间持有流(Avoid retaining streams for too long)

    保留RACStream超过必要的时间会引发关于保留的依赖问题,如内存使用过高等。

    RACSequence应该只被保留序列的head需要被保留的那么长时间。如果 head 不再被使用,保留节点的 tail 代替节点本身。

    参见Memory Management指引获取关于对象生命周期的更多信息。

    2.3.5 只处理需要数量的流(Process only as much of a stream as needed)

    让流或者RACSignal订阅保持不必要的活跃状态会导致cpu使用增长。

    如果流中只有特定数量的值需要被用到,-take:操作可以用来返回这些值,然后返回值之后立即自动结束流。

    类似-take:-takeUntil等操作能自动释放栈。如果其余的值不再需要,任何依赖也会结束,这可以显著的减少潜在的开销。

    2.3.6 分发信号事件到一个已知的调度(Deliver signal events onto a known scheduler)

    当信号被一个方法返回,或者被信号组合,很难搞清楚是在哪个线程上事件被分发。 尽管事件被确保是串行的,但有时候需要更严格的情形,比如 UI 的刷新必须在主线程。

    无论何时保证事件是串行的都很重要,-deliverOn:操作应该被用来强制信号事件到达一个明确的RACScheduler

    2.3.7 (较少的场合需要切换调度者)Switch schedulers in as few places as possible

    在满足上面的情况下,事件还应该在必要的时候分发到明确的scheduler。切换调度这会引入不必要的时延和 cpu 负担。

    通常,使用-deliverOn:应该被限制在信号链的末端。例如,在订阅之前,或者在被绑定到一个属性之前。

    2.3.8 明确信号的副作用(Make the side effects of a signal explicit)

    RACSignal的副作用应该尽可能避免,因为订阅可能出现行为副作用异常。

    然而,有时候信号时间发生时,副作用是有用的。 尽管大多数RACStreamRACSignal操作接受任意的 block (有副作用的), 使用-doNext:,-doError:,-doCpmpleted:能更明确和自解释副作用的发生。

    NSMutableArray *nexts = [NSMutableArray array];
    __block NSError *receivedError = nil;
    __block BOOL success = NO;
    
    RACSignal *bookkeepingSignal = [[[valueSignal
        doNext:^(id x) {
            [nexts addObject:x];
        }]
        doError:^(NSError *error) {
            receivedError = error;
        }]
        doCompleted:^{
            success = YES;
        }];
    
    RAC(self,value) = bookkeepingSignal;

    2.3.9 用多播共享信号副作用(Share the side effects of a signal by multicasting)

    默认情况下,副作用在每次订阅的时候发生,但在某些特定的情况下副作用应够只发生一次--例如, 一个网络请求很明显不应该在添加新的订阅的时候重复调用

    RACSignal-publish-multicast:操作允许一个单一的订阅通过使用RACMulticastConnection共享给多个订阅者。

    // This signal starts a new request on each subscription.
    RACSignal *networkRequest = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        AFHTTPRequestOperation *operation = [client
            HTTPRequestOperationWithRequest:request
            success:^(AFHTTPRequestOperation *operation,id response) {
                [subscriber sendNext:response];
                [subscriber sendCompleted];
            }
            failure:^(AFHTTPRequestOperation *operation,NSError *error) {
                [subscriber sendError:error];
            }];
    
        [client enqueueHTTPRequestOperation:operation];
        return [RACDisposable disposableWithBlock:^{
            [operation cancel];
        }];
    }];
    
    // Starts a single request,no matter how many subscriptions `connection.signal`
    // gets. This is equivalent to the -replay operator,or similar to
    // +startEagerlyWithScheduler:block:.
    RACMulticastConnection *connection = [networkRequest multicast:[RACReplaySubject subject]];
    [connection connect];
    
    [connection.signal subscribeNext:^(id response) {
        NSLog(@"subscriber one: %@",response);
    }];
    
    [connection.signal subscribeNext:^(id response) {
        NSLog(@"subscriber two: %@",response);
    }];

    2.3.10 通过给定的名字调试流(Debug streams by giving them names)

    每一个RACStream有一个name属性用来协助调试。 流的description包含流的名称,并且 RAC 所有的操作都会添加这个名称。从名称可以很方便的标识出一个流。

    例如如下代码片段:

    RACSignal *signal = [[[RACObserve(self,username) 
        distinctUntilChanged] 
        take:3] 
        filter:^(NSString *newUsername) {
            return [newUsername isEqualToString:@"joshaber"];
        }];
    
    NSLog(@"%@",signal);

    上面的代码会记录一个类似[[[RACObserve(self,username)] -distinctUntilChanged] -take: 3] -filter:名称

    名称也可以通过-setNameWithFormat:手工添加

    RACSignal也提供-logNext,-logError,-logCompleted-logAll方法, 这些方法在事件发生时自动记录信号事件,包括信号名称和消息。这可以为实时观察信号提供便利。

    2.3.11 避免明确的订阅和释放(Avoid explicit subscriptions and disposal)

    尽管-subscribeNext:error:completed:和它的变体是处理信号的最基本的方式,但它们使用较少的声明导致代码复杂, 推荐使用副作用,尽量复用潜在的内建功能

    同样的,明确的使用RACDisposable类能快速导致老鼠窝一样(啥意思,一团糟的意思吗?)的资源管理和代码清除。

    下面是几乎总是该遵循的高级模式,用来替换手动订阅和释放:

    • RACRACChannelTo宏能够用来绑定信号到一个属性,用来代替在改变发生时手动更新的机制。
    • -rac_liftSelector:withSignals:方法能够用来在信号触发时自动调用一个 selector 。
    • -takeUntil:之类的操作在时间发生时能够用来自动释放订阅(例如 UI 的‘取消’按钮被按下)。

    通常,相比在订阅的回调中完成相同功能,使用streamsignal内建的操作只需更简单更少出错的代码

    2.3.12 尽可能避免使用 subjects(Avoid using subjects when possible)

    Subjects是信号用来桥接命令式代码和现实世界的一个强有力的工具。但是对于可变 RAC 来说,他们的过度使用很快会导致代码复杂。

    因为 Subjects 可以在任何地方任何时间使用,所以 subjects 经常打破 stream 的线性处理,导致逻辑复杂。 Subjects 也不支持严格的 disposal,严格的 disposal 会引入不必要的任务。

    Subjects 能够被 ReactiveCocoa 的下列其他模式替换:

    • 考虑用+createSignalblock 生成值 来代替 提供初始化值到一个 subject 中。
    • 考虑用+combineLatest:+zip:等操作合并多个信号的输出 来代替 分发中间结果给 subject。
    • 考虑用multicast多播基本的信号 来代替 使用subjects共享多个订阅的结果。
    • 考虑用command-rac_signalForSelector:来代替 实现多个动作方法来实现对 subject 的简单控制。

    当 subject 必须使用时,他们几乎总是被使用在信号链的基本输入,而不是在信号链中间使用。

    2.4 实现一个新的操作(Implementing new operators)

    RAC 为streamsignal提供了大量内建操作,能够满足大部分应用场景;然而,RAC 不是一个封闭的系统。 ReactiveCocoa 考虑了为了特殊的用途实现一些额外的操作。

    实现新的操作需要特别注意一些细节和简单化操作,避免在调用代码中引入bug。

    下面的指南包括一些通用的原则能够帮助编写符合预期的 API:

    2.4.1 优先使用基于 RACStream 的方法(Prefer building on RACStream methods)

    RACStream提供的接口比RACSequenceRACSignal更简单,而且所有的 stream 操作也适用于 sequence 和 signal。

    基于这个原因,无论何时新操作都应该基于RACStream方法实现。 至少需要RACStream类的-bind:,-zipWith:,和-concat:方法,这些方法就已经很强大了, 不需要添加其他任何功能就可以完成很多任务。

    如果一个新的RACSignal操作需要处理errorcompleted事件, 考虑使用-materialize方法给 stream 引入事件。 所有 materialized 的信号事件都能够被流操作修改,这能帮助最小化使用非 stream 的操作(???这句话没整明白)。

    2.4.2 尽可能组合已存在的操作(Compose existing operators when possible)

    RAC 经过了深思熟虑,通过了合法性测试,也在很多项目中被使用。重新改写操作的代码可能不会有很好的健壮性,或者不能处理一些内建操作已经考虑到的特殊情况。

    为了最小的重复代码和尽可能少的引入 bug,在自定义的操作实现中,尽可能使用已提供的功能。通常只有很少的代码需要重写。

    2.4.3 避免引入并发(Avoid introducing concurrency)

    在编程中,并发是非常容易引入 bug 的。为了尽可能的避免死锁和竞态,不应该引入并发。

    调用者可以在一个明确的RACScheduler订阅和分发事件,RAC 提供强大的parallelize work方式而不会特别复杂。

    2.4.4 在 disposable 中取消任务和清理所有资源(Cancel work and clean up all resources in a disposable)

    使用+createSignal:方法创建一个信号时,它提供的 block 需要返回一个RACDisposable。 该 disposable 可以:

    • 可以方便的取消信号开始的任务。
    • 立即 dispose 到其他信号的订阅,然后触发取消和清理。
    • 释放信号分配的内存和其它资源。

      这有助于实现RACSignal 的约定

    2.4.5 操作中不要阻塞(Do not block in an operator)

    流操作应该立即返回一个新流。任何操作需要完成的工作应该是新流的运算的一部分,而不是调用的流自身的一部分。

    // WRONG!
    - (RACSequence *)map:(id (^)(id))block {
        RACSequence *result = [RACSequence empty];
        for (id obj in self) {
            id mappedObj = block(obj);
            result = [result concat:[RACSequence return:mappedObj]];
        }
    
        return result;
    }
    
    // Right!
    - (RACSequence *)map:(id (^)(id))block {
        return [self flattenMap:^(id obj) {
            id mappedObj = block(obj);
            return [RACSequence return:mappedObj];
        }];
    }

    如果要从流中返回一个或多个值(例如first),该规则可以忽略掉。

    2.4.6 避免深度递归导致栈溢出(Avoid stack overflow from deep recursion)

    任何无限递归操作都需要使用RACSchedulershceduleRecusiveBlock:方法。该方法会将递归转换为迭代操作,防止栈溢出。

    例如,下面是-repeat的一个错误的实现,肯定会导致栈溢出和崩溃:

    - (RACSignal *)repeat {
        return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
            RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];
    
            __block void (^resubscribe)(void) = ^{
                RACDisposable *disposable = [self subscribeNext:^(id x) {
                    [subscriber sendNext:x];
                } error:^(NSError *error) {
                    [subscriber sendError:error];
                } completed:^{
                    resubscribe();
                }];
    
                [compoundDisposable addDisposable:disposable];
            };
    
            return compoundDisposable;
        }];
    }

    而下面的版本就会避免栈溢出:

    - (RACSignal *)repeat {
        return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
            RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];
    
            RACScheduler *scheduler = RACScheduler.currentScheduler ?: [RACScheduler scheduler];
            RACDisposable *disposable = [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) {
                RACDisposable *disposable = [self subscribeNext:^(id x) {
                    [subscriber sendNext:x];
                } error:^(NSError *error) {
                    [subscriber sendError:error];
                } completed:^{
                    reschedule();
                }];
    
                [compoundDisposable addDisposable:disposable];
            }];
    
            [compoundDisposable addDisposable:disposable];
            return compoundDisposable;
        }];
    }

    3 与 Rx 的差异(Differences from Rx)

    ReactiveCocoa (RAC) 深受 .NET 的Reactive Extensions(Rx)影响,但不是直接的移植。 RAC 的一些原则和接口对于已经熟悉 Rx 的开发者来说都会迷惑,但还是表达了相同的算法。

    一些不同之处,像方法和类的命名,是为了符合 Cocoa 现有的风格。 还有一些是在 Rx 上的改进,或者从其他函数式响应式编程范式中借鉴的(例如 Elm 编程语言)。

    下面,尝试讲解 RAC 和 Rx 的不同。

    3.1 接口(Interfaces)

    EAC 不提供类似 .NET 中的IEnumerableIObserver接口。RAC 中主要用三个类来代替:

    • RACStream实现了基本流操作的抽象类。 实现了常用的 LINQ(Language Integrated Query,C# 术语,用于方便的执行一些增删查改等)操作。
    • RACSignalRACStream的一个具体的子类,实现了pish-driven流,跟IObserver很像。 在该类或RACSignal+Operations类别中可以找到基于时间的操作,或者处理completederror事件的方法
    • RACSequence也是RACStream的一个具体的子类实现了pull-driven流,跟IEnumerable很像。

    3.2 流操作的名称(Names of Stream Operations)

    RAC 通常使用 LINQ 风格命名流方法。大多数异常处理深受 Haskell 和 Elm 的影响。

    注意如下不同:

    • -map:代替Select
    • -filter:代替Where
    • -flatten代替Merge
    • -flattenMap:代替SelectMany

    LINQ 操作在 RAC 中名称有变化(但行为或多或少相同),在文档中有记载,例如:

    // Maps `block` across the values in the receiver.
    //
    // This corresponds to the `Select` method in Rx.
    //
    // Returns a new stream with the mapped values.
    - (instancetype)map:(id (^)(id value))block;

    4 框架概览(Framework Overview)

    本文包含 ReactiveCocoa 框架不同组件的一些高层次的描述,并且尝试说明他们如何在一起工作,然后分别承担什么职责。 这意味着要先理解本文,然后才学习其他新模块和其他特定文档。

    范例和如何使用 RAC,参见READMEDesign Guidelines

    4.1 流(Streams)

    RACStream抽象类用来描述流,是存储对象值得所有序列。

    值可以立即获得,也可以在未来某个时间获得,但必须是按顺序获取。在没有计算或等到第一个值之前是无法获取第二个值的。

    流是游离的(monads,游牧的)?还允许基于一些基本操作构建复杂的操作(特别是-bind:)。 RACStream 也从Haskell实现了MonoidMonadZip类型类。??

    RACStream自身并不是特别有用。大多数流被signalsequences替代。

    4.2 信号(Signals)

    signal,由RACSignal表示,是push-dirven类型的流。

    信号通常用来表示未来可能会被分发的数据。当任务完成或者数据被接受,值会被发送到信号,信号则推送他们到任何订阅者。 用户必须订阅(subscribe)信号才能访问信号的值。

    信号提供给订阅者三种不同类型的事件:

    • next事件提供流中的一个新值。RACStream方法只操作这种类型的事件。
    • 不像 Cocoa 集合,它完全有效的信号是包括nil的.
    • error事件表明在信号完成之前发生了错误。该事件会包含一个NSError对象表明是什么错误
    • 错误必须特殊处理--错误不包含流中的值。
    • completed事件表明信号成功完成了,并且完成之后不会再有值会被添加到流中。完成操作必须特殊处理--完成不包含流的值。

    信号的生命周期由任意数量next组成,跟随者错误(error)完成(completed)(错误和完成二者只存在一个,不会同时存在)。

    4.2.1 订阅(Subscription)

    订阅是唯一能从信号中等待或者能够从信号中等待事件的主体。在 RAC 中,订阅是任何遵从RACSubscriber协议的对象。

    订阅-subscribeNext:error:completed:或其他相应的方法中创建。 严格来说,大多数RACStreamRACSignal操作也能够创建订阅, 但这些中间状态的订阅通常只是一个实现的细节(不是很明白)??

    订阅会保留他们订阅的信号,不会自动释放除非信号完成或者出错。订阅也能够手动释放。

    4.2.2 Subjects

    subjectRACSubject来描述,是可以被手动控制的信号类型。

    Subjects 可以认为是可变的信号,类似NSMutableArrayNSArray的关系。在桥接非 RAC 代码和信号时很有用。

    例如,处理应用逻辑的block,可以用发送事件到共享 subject 的 block 代替。 subject 随后返回一个RACSignal,隐藏 block 的实现细节。

    某些 subject 提供额外的功能RACReplaySubject能够用来为未来的订阅者缓存事件, 就像网络请求完成之前处理结果的东西都已准备好。

    4.2.3 命令(Commands)

    commandRACCommand类来表示,为某些动作响应创建和订阅信号。命令让用户与 app 交互实现副作用变的很容易。

    通常行为触发命令是由 UI 驱动的,例如一个按钮被按下。 基于信号的命令能够自动被禁用,这个禁用状态能够用 UI 中其他任何跟命令相关的控件来描述。

    在 OS X 中,RAC 添加了一个rac_command属性NSButton用来自动设置按钮的行为。

    4.2.4 连接(Connections)

    连接RACMulticastConnectiong类来描述,是可以在任意数量订阅者之间共享的订阅

    信号默认是cold,意味着他们在每一次新订阅添加的时候开始工作。 这个行为通常是期望的,因为数据会为每个订阅者刷新和重新计算,但是如果信号有副作用或者任务代价很昂贵就会引入一些问题 (例如发送网络请求)。

    连接通过RACSignal类的-publish或者-multicast:方法创建, 并且不管连接有多少次订阅,确保底层只有一个订阅被创建。一旦连接建立,连接的信号宣告是hot类型, 在这底层的订阅会保留活动状态直到连接的所有订阅都被释放。

    4.3 序列(Sequences)

    序列RACSequence类来描述,是pull-driven类型的流。

    序列是一个集合,累世完成NSArray功能。 跟 array 不一样的是,序列的值默认是lazily的,只在序列被需要的时候才会计算,提高了效率。 序列不能包含nil

    序列类似闭包的序列或者Haskell中的List类型。

    RAC 添加-rac_qequence方法到大多数 Cocoa 的集合类,允许他们使用RACSequences来替代。

    4.4 释放(Disposables)

    RACDisposable类用来取消任务和资源清理。

    Disposables 常用于信号的取消订阅。 当订阅被释放,响应的订阅者不会再收到信号的任何未来事件。 并且任何订阅相关的工作(后台处理,网络请求等)都会取消,因为结果不再需要了。

    关于取消的更多信息,参见 RACDesign Guidelines

    4.5 调度(Schedulers)

    调度RACScheduler类来描述,是一个串行执行队列,信号在上面完成任务和分发结果。

    调度类似 GCD 队列,但调度支持取消,并且总是串行执行。+immediateScheduler异常时,调度不会提供同步执行。这可以避免死锁,鼓励使用信号操作代替 block。

    RACScheduler有些方面也像NSOperatiaonQueue,但调度不允许任务重新排序,也不支持依赖。

    4.6 值类型(Value types)

    RAC 提供少量杂项类来方便表达流中的值:

    • RACTuple是个小的,固定带笑傲的集合,能够包含nil(用RACTupleNil表示)。经常用来表示多个流中合并的值。
    • RACUnit是空值得单例。用来表示流中某些时候没有存在意义的数据。
    • RACEvent表示任意信号事件。主要被RACSignal类的-materialize方法使用。

    5. 内存管理(Memory Management)

    ReactiveCocoa 的内存管理非常复杂,但最终结果是处理信号时,你并不需要保留他们

    如果框架要求你保留每一个信号,那这个框架使用起来就太笨重,特别是一次性的信号用于未来某个时候。 你不需要保留任何长时间活跃的信号到属性中,然后确保用完之后再清除。这样可没劲了。

    5.1 订阅者(Subscribers)

    无论去哪之前,subscribeNext:error:completed:(还有所有其所有变量)创建一个隐式订阅者使用给定的block。 任何这些 block 引用的对象会被保留为订阅的一部分。就像其他对象,self不会被保留除非有一个对它直接或间接的引用。

    5.2 有限或短暂的信号(Finite or Short-Lived Signals)

    RAC 内存管理最重要的原则是订阅在完成后错误自动终止,订阅者会被移除

    例如,如果你的 view controller 中的代码如下:

    self.disposable = [signal subscribeCompleted:^{
        doSomethingPossiblyInvolving(self);
    }];

    … the memory management will look something like the following:

    那么内存管理会是下面的流程:

    view controller -> RACDisposable -> RACSignal -> RACSubscriber -> view controller

    然而,RACSignal -> RACSubscriber的关系会在信号完成时立刻解除,打破保留环。

    这是你需要的,因为RACSignal的生命周期会自然而然的匹配时间流的逻辑生命周期。

    5.3 无限信号(Infinite Signals)

    Infinite signals (or signals that live so long that they might as well be infinite),however,will never tear down naturally. This is where disposables shine.

    Disposing of a subscription will remove the associated subscriber,and just

    无限信号(或者说永远存活的信号),不会被自动清除。

    释放订阅会移除相关的订阅,并且会清除订阅相关的任何资源。

    作为一个一般的经验法则,如果你需要手动管理订阅的生命周期,那可能存在更好的方式做到你想要的,请避免显示订阅和释放。

    5.4 从self分发的信号(Signals Derived fromself

    还是有些比较棘手的情况的。任何时候一个信号的生命周期被绑在一个调用范围时,会比较难打破循环。

    这种情况通常发生在关键路径上使用RACObserve(),而关键路径又与self关联,然后应用的 block 有需要捕获self

    最简单的解决方案是捕获 self 弱引用

    __weak id weakSelf = self;
    [RACObserve(self,username) subscribeNext:^(NSString *username) {
        id strongSelf = weakSelf;
        [strongSelf validateUsername];
    }];

    或者,在导入EXTScope.h文件之后:

    @weakify(self);
    [RACObserve(self,username) subscribeNext:^(NSString *username) {
        @strongify(self);
        [self validateUsername];
    }];

    如果对象不支持若引用,那么用__unsafe_unretained@unsafeify分别替换__weak@weakify例如,上面的示例可以像下面这么写:

    [self rac_liftSelector:@selector(validateUsername:) withSignals:RACObserve(self,username),nil];

    或者:

    RACSignal *validated = [RACObserve(self,username) map:^(NSString *username) {
        // Put validation logic here.
        return @YES;
    }];

    无限的信号,通常可以从信号链的 block 避开引用self(或任何对象)。


    上面的信息是高效使用 ReactiveCocoa 所需要的一切。然而,还有很多只为技术上的好奇心或者对 RAC 感兴趣的声音存在。

    “不需要保留”的设计目标引入如下问题:我们如何知道信号什么时候需要被释放?如果信号刚创建,没被自动释放池管理,没有被保留的话。

    正确的回答是我们不需要,但我们通常确保调用者如果想保留信号,会在当前运行循环迭代中保留它。

    因此:

    @H_404_8@
  • 一个被创建的信号自动添加到激活信号集合中。
  • 信号会等一个主运行循环,然后如果没有订阅订阅它就会从激活信号集中移除。除非信号被什么保留了,否则会被释放。
  • 如果在运行循环迭代中订阅发生了,信号会留在集合中。
  • 最后,如果所有订阅者已过去,步骤2就会再被触发。
  • 如果运行循环是spun recursively(纺递归是什么鬼?就像 OS X中的模态事件)那就适得其反了。但是让框架的用户使用方便比任何其他事情都重要。

    猜你在找的React相关文章