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

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

1. 基本操作(Basic Operators)

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

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

  1. 订阅(Subscription)
  2. 依赖注入(Injecting effects)
@H_403_2@流的传输(Transforming streams)

  1. 映射(Mapping)
  2. 过滤(Filtering)
@H_403_2@流的结合(Combining streams)

  1. 串联(Concatenating)
  2. 压缩(Flattening)
  3. 映射和压缩(Mapping and flattening)
@H_403_2@信号的结合(Combining signals)

  1. 排序(Sequencing)
  2. 合并(Merging)
  3. 结合最新值(Combining latest values)
  4. 切换(Switching)

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

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

  • 冷信号:被订阅的时候才激活。信号默认是冷类型的。
  • 热信号:返回给调用者的时候已经被激活。
@H_403_2@译者注:什么是push-driven? 什么是pull-driven?

  • push-driven:在创建信号的时候,信号不会被立即赋值,之后才会被赋值(例如网络请求回来的结果或者是任意的用户输入的结果)。
  • pull-driven:在创建信号的时候,序列中的值就会被确定下来,我们可以从流中一个个的查询值。
@H_403_2@大多数信号(signals)初始化是冷(cold)信号,这意味着它们在被订阅(Subscription)之前不会做任何工作。
订阅之后,信号或者它的订阅者能够完成边界效应,比如输出日志到控制台,做一个网络请求,修改用户界面等。
副作用也可以被注入到一个信号,这样的副作用不会立刻完成,但是会被稍后的每一个订阅者触发副作用。

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

1.1.1 订阅(Subscription)

@H_403_2@-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);
}];
@H_403_2@对于冷信号来说,副作用会在每一次订阅时发生:

__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);
}];
@H_403_2@行为可以被connection(后面会讲到connection) 改变.

1.1.2 注入影响(Injecting effects)

@H_403_2@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)

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

1.2.1 映射(Mapping)

@H_403_2@-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)

@H_403_2@-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)

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

1.3.1 串联(Concatenating)

@H_403_2@-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,这个真不知道怎么翻译)

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

@H_403_2@序列是串联(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];
@H_403_2@信号是合并(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)

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

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

@H_403_2@可用来扩展或编辑序列:

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]; 
    }
}];
@H_403_2@或者创建多个信号自动合并:

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)

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

1.4.1 序列化(Sequencing)

@H_403_2@-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;
    }];
@H_403_2@这在某些情况下很有用,比如执行一个信号的所有副作用,然后开始另外一个信号,并且只返回第二个信号的值。

1.4.2 合并(Merging)

@H_403_2@+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)

@H_403_2@+ 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"];
@H_403_2@注意结合信号会只发送第一个值,当所有输入被发送至少一个的时候。上面的例子中,@"A"不会被转发因为number没有被发送一个值。

1.4.4 切换(Switching)

@H_403_2@-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)

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

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

@H_403_2@RACSequence的约定

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

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

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

  1. 优先使用基于 RACStream 方法
  2. 尽可能组合已存在的operators。
  3. 避免引入并发。
  4. 在dispasable中取消任务和清理所有资源。
  5. 在operator中不要阻塞。
  6. 深度递归要避免栈溢出。

2.1 RACSequence的约定(The RACSequence contract)

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

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

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

NSArray *strings = @[ @"A",@"B",@"C" ];
RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
    return [str stringByAppendingString:@"_"];
}];
@H_403_2@没有字符串会被实际追加直到序列真正需要的时候。 访问sequence.head会完成A_的拼接,访问sequence.tail.head会完成B_的拼接,等等。

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

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

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

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

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

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

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

@H_403_2@当传递给序列操作的 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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@H_403_2@考虑如下代码

__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);
}];
@H_403_2@副作用会在每次订阅的时候重复发生。同样适用于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);
}];
@H_403_2@要阻止上述行为,在多次订阅一个信号时只执行它的副作用一次,可以用信号的多播功能multicasted

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

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

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

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

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

2.3 Best practices

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

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

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

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

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

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

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

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

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

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

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

RACStream *result = [stream startWith:@0];

RACStream *result2 = [stream map:^(NSNumber *value) {
    return @(value.integerValue + 1);
}];
@H_403_2@如果传输同一个流多次,确保每一个步骤都是对齐的。 复杂的操作比如+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);
    }];
@H_403_2@当然,带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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@H_403_2@然而,有时候信号时间发生时,副作用是有用的。 尽管大多数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)

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

@H_403_2@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)

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

@H_403_2@例如如下代码片段:

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

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

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

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

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

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

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

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

  • RACRACChannelTo宏能够用来绑定信号到一个属性,用来代替在改变发生时手动更新的机制。
  • -rac_liftSelector:withSignals:方法能够用来在信号触发时自动调用一个 selector 。
  • -takeUntil:之类的操作在时间发生时能够用来自动释放订阅(例如 UI 的‘取消’按钮被按下)。
@H_403_2@通常,相比在订阅的回调中完成相同功能,使用streamsignal内建的操作只需更简单更少出错的代码

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

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

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

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

  • 考虑用+createSignalblock 生成值 来代替 提供初始化值到一个 subject 中。
  • 考虑用+combineLatest:+zip:等操作合并多个信号的输出 来代替 分发中间结果给 subject。
  • 考虑用multicast多播基本的信号 来代替 使用subjects共享多个订阅的结果。
  • 考虑用command-rac_signalForSelector:来代替 实现多个动作方法来实现对 subject 的简单控制。
@H_403_2@当 subject 必须使用时,他们几乎总是被使用在信号链的基本输入,而不是在信号链中间使用。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @H_403_2@这有助于实现RACSignal 的约定

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

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

// 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];
    }];
}
@H_403_2@如果要从流中返回一个或多个值(例如first),该规则可以忽略掉。

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

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

@H_403_2@例如,下面是-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;
    }];
}
@H_403_2@而下面的版本就会避免栈溢出:

- (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)

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

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

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

3.1 接口(Interfaces)

@H_403_2@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)

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

@H_403_2@注意如下不同:

  • -map:代替Select
  • -filter:代替Where
  • -flatten代替Merge
  • -flattenMap:代替SelectMany
@H_403_2@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)

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

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

4.1 流(Streams)

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

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

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

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

4.2 信号(Signals)

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

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

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

  • next事件提供流中的一个新值。RACStream方法只操作这种类型的事件。
  • 不像 Cocoa 集合,它完全有效的信号是包括nil的.
  • error事件表明在信号完成之前发生了错误。该事件会包含一个NSError对象表明是什么错误
  • 错误必须特殊处理--错误不包含流中的值。
  • completed事件表明信号成功完成了,并且完成之后不会再有值会被添加到流中。完成操作必须特殊处理--完成不包含流的值。
@H_403_2@信号的生命周期由任意数量next组成,跟随者错误(error)完成(completed)(错误和完成二者只存在一个,不会同时存在)。

4.2.1 订阅(Subscription)

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

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

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

4.2.2 Subjects

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

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

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

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

4.2.3 命令(Commands)

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

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

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

4.2.4 连接(Connections)

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

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

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

4.3 序列(Sequences)

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

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

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

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

4.4 释放(Disposables)

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

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

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

4.5 调度(Schedulers)

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

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

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

4.6 值类型(Value types)

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

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

5. 内存管理(Memory Management)

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

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

5.1 订阅者(Subscribers)

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

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

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

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

self.disposable = [signal subscribeCompleted:^{
    doSomethingPossiblyInvolving(self);
}];
@H_403_2@… the memory management will look something like the following:

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

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

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

5.3 无限信号(Infinite Signals)

@H_403_2@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.

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

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

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

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

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

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

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

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

__weak id weakSelf = self;
[RACObserve(self,username) subscribeNext:^(NSString *username) {
    id strongSelf = weakSelf;
    [strongSelf validateUsername];
}];
@H_403_2@或者,在导入EXTScope.h文件之后:

@weakify(self);
[RACObserve(self,username) subscribeNext:^(NSString *username) {
    @strongify(self);
    [self validateUsername];
}];
@H_403_2@如果对象不支持若引用,那么用__unsafe_unretained@unsafeify分别替换__weak@weakify例如,上面的示例可以像下面这么写:

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

RACSignal *validated = [RACObserve(self,username) map:^(NSString *username) {
    // Put validation logic here.
    return @YES;
}];
@H_403_2@无限的信号,通常可以从信号链的 block 避开引用self(或任何对象)。


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

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

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

@H_403_2@因此:

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

猜你在找的React相关文章