RAC双向绑定
简介
在 ReactiveObjC 中,根据数据流的方向,我们可以划分出两种不同数据流,即:单向数据流,如:RACSignal、RACSubject、RACMulticastConnection;双向数据流,如:RACChannel、RACKVOChannel。这篇文章主要介绍 ReactiveObjC 中的双向数据流。当我们需要实现数据的双向绑定时(A的改动影响B,B的改动也影响A),使用 ReactiveObjC 提供的双向数据流可以很方便的实现相关需求。
RACChannel
RACChannel 类似一个双向连接,连接的两端都是 RACSignal 实例。RACChannel 像一个魔法盒子,我们可以在A端发送信号,在B端订阅A端的信号,也可以在B端发送信号,在A端订阅B端的信号。如下所示:
RACChannel.png1、RACChannelTerminal
在看 RACChannel 的源码之前,我们需要先了解 RACChannelTerminal 这个类。之所以需要了解它,是因为 RACChannel 有两个重要属性 leadingTerminal、followingTerminal,它们分别代表了 RACChannel 的两端,是实现 RACChannel 的关键,而这两个属性都是 RACChannelTerminal 类型。
RACChannelTerminal 类定义如下:
@interface RACChannelTerminal<ValueType> : RACSignal<ValueType> <RACSubscriber>
- (instancetype)init __attribute__((unavailable("Instantiate a RACChannel instead")));
// Redeclaration of the RACSubscriber method. Made in order to specify a generic type.
- (void)sendNext:(nullable ValueType)value;
@end
从定义可以看出 RACChannelTerminal 继承自RACSignal ,说明它可以被订阅,同时实现了 RACSubscriber 协议,说明它可以发送消息。接下来看看RACChannelTerminal 的具体实现:
RACChannelTerminal 实现:
@implementation RACChannelTerminal
#pragma mark Lifecycle
- (instancetype)initWithValues:(RACSignal *)values otherTerminal:(id<RACSubscriber>)otherTerminal {
NSCParameterAssert(values != nil);
NSCParameterAssert(otherTerminal != nil);
self = [super init];
// 初始化两个端点属性
_values = values;
_otherTerminal = otherTerminal;
return self;
}
#pragma mark RACSignal
// 订阅时,实际上被订阅的是self.values信号
- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
return [self.values subscribe:subscriber];
}
#pragma mark <RACSubscriber>
// 发送时,实际上是用self.otherTerminal 来发送消息
- (void)sendNext:(id)value {
[self.otherTerminal sendNext:value];
}
- (void)sendError:(NSError *)error {
[self.otherTerminal sendError:error];
}
- (void)sendCompleted {
[self.otherTerminal sendCompleted];
}
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable {
[self.otherTerminal didSubscribeWithDisposable:disposable];
}
@end
在初始化时,RACChannelTerminal 需要传入 values 和 otherTerminal 两个值,其中 values、otherTerminal 分别表示 RACChannelTerminal 的两个端点。在订阅者调用 -subscribeNext:
等方法发起订阅时,实际上订阅的是self.values 信号;如果向当前端点发送消息,会使用 self.otherTerminal 来发送消息。由于不是使用 self.values 的订阅者来发送消息,因此,self.values 也就收不到 RACChannelTerminal 发送的消息。原理图如下:
2、RACChannel
了解了 RACChannelTerminal 之后,我们再来看 RACChannel 的实现,从源码可以看出 RACChannel 有两个属性leadingTerminal、followingTerminal,他们分别代表了 RACChannel 的两端,这两个属性都是 RACChannelTerminal 类型。
@interface RACChannel<ValueType> : NSObject
@property (nonatomic, strong, readonly) RACChannelTerminal<ValueType> *leadingTerminal;
@property (nonatomic, strong, readonly) RACChannelTerminal<ValueType> *followingTerminal;
@end
接下来,我们看 RACChannel 的具体实现:
- (instancetype)init {
self = [super init];
// We don't want any starting value from the leadingSubject, but we do want
// error and completion to be replayed.
RACReplaySubject *leadingSubject = [[RACReplaySubject replaySubjectWithCapacity:0] setNameWithFormat:@"leadingSubject"];
RACReplaySubject *followingSubject = [[RACReplaySubject replaySubjectWithCapacity:1] setNameWithFormat:@"followingSubject"];
// Propagate errors and completion to everything.
[[leadingSubject ignoreValues] subscribe:followingSubject];
[[followingSubject ignoreValues] subscribe:leadingSubject];
_leadingTerminal = [[[RACChannelTerminal alloc] initWithValues:leadingSubject otherTerminal:followingSubject] setNameWithFormat:@"leadingTerminal"];
_followingTerminal = [[[RACChannelTerminal alloc] initWithValues:followingSubject otherTerminal:leadingSubject] setNameWithFormat:@"followingTerminal"];
return self;
}
可以看出,RACChannel 初始化的时候,实际上只创建了两个 RACReplaySubject 热信号。初始化 _leadingTerminal 和 _followingTerminal 两个属性时,只是交换了两个 RACReplaySubject 的顺序,因为两 RACReplaySubject 是热信号,它们既可以作为订阅者,也可以接收其他对象发送的消息。通过 -ignoreValues
和 -subscribe:
方法,leadingSubject 和 followingSubject 两个热信号中产生的错误会互相发送,目的是为了防止一边发生了错误,另一边还继续工作。原理图如下:
RACChannel 内部的双箭头表示这两个 RACReplaySubject 为同一个热信号,由于只创建了两个 RACReplaySubject 热信号,因此,在两个 RACChannelTerminal 中,只是交换了_values 和 _otherTerminal 的位置。
双向绑定
- 使用 RACChannel 实现双向绑定。我们需要在 _leadingTerminal 端和 _followingTerminal 端分别实现订阅和发送。
RACChannel *channel = [[RACChannel alloc] init];
RAC(self, a) = channel.leadingTerminal;
[RACObserve(self, a) subscribe:channel.leadingTerminal];
RAC(self, b) = channel.followingTerminal;
[RACObserve(self, b) subscribe:channel.followingTerminal];
不过遗憾的是会出现堆栈溢出的错误,为什么呢?因为 RACChannel 只是实现了双向绑定,并没有帮我们处理循环调用的问题。在这里A的改动会影响B,B的改动也会影响A,就这样无限循环下去。
RACKVOChannel
直接使用 RACChannel,可能会出现堆栈溢出的错误。因此,我们需要打断这种死循环。这时候,我们就需要使用 RACKVOChannel 来实现双向绑定了。RACKVOChannel 继承自 RACChannel。接下来看一下它的初始化:
- (instancetype)initWithTarget:(__weak NSObject *)target keyPath:(NSString *)keyPath nilValue:(id)nilValue {
NSCParameterAssert(keyPath.rac_keyPathComponents.count > 0);
NSObject *strongTarget = target;
self = [super init];
_target = target;
_keyPath = [keyPath copy];
[self.leadingTerminal setNameWithFormat:@"[-initWithTarget: %@ keyPath: %@ nilValue: %@] -leadingTerminal", target, keyPath, nilValue];
[self.followingTerminal setNameWithFormat:@"[-initWithTarget: %@ keyPath: %@ nilValue: %@] -followingTerminal", target, keyPath, nilValue];
if (strongTarget == nil) {
[self.leadingTerminal sendCompleted];
return self;
}
// Observe the key path on target for changes and forward the changes to the
// terminal.
//
// Intentionally capturing `self` strongly in the blocks below, so the
// channel object stays alive while observing.
RACDisposable *observationDisposable = [strongTarget rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent) {
// If the change wasn't triggered by deallocation, only affects the last
// path component, and ignoreNextUpdate is set, then it was triggered by
// this channel and should not be forwarded.
if (!causedByDealloc && affectedOnlyLastComponent && self.currentThreadData.ignoreNextUpdate) {
[self destroyCurrentThreadData];
return;
}
[self.leadingTerminal sendNext:value];
}];
NSString *keyPathByDeletingLastKeyPathComponent = keyPath.rac_keyPathByDeletingLastKeyPathComponent;
NSArray *keyPathComponents = keyPath.rac_keyPathComponents;
NSUInteger keyPathComponentsCount = keyPathComponents.count;
NSString *lastKeyPathComponent = keyPathComponents.lastObject;
// Update the value of the property with the values received.
[[self.leadingTerminal
finally:^{
[observationDisposable dispose];
}]
subscribeNext:^(id x) {
// Check the value of the second to last key path component. Since the
// channel can only update the value of a property on an object, and not
// update intermediate objects, it can only update the value of the whole
// key path if this object is not nil.
NSObject *object = (keyPathComponentsCount > 1 ? [self.target valueForKeyPath:keyPathByDeletingLastKeyPathComponent] : self.target);
if (object == nil) return;
// Set the ignoreNextUpdate flag before setting the value so this channel
// ignores the value in the subsequent -didChangeValueForKey: callback.
[self createCurrentThreadData];
self.currentThreadData.ignoreNextUpdate = YES;
[object setValue:x ?: nilValue forKey:lastKeyPathComponent];
} error:^(NSError *error) {
NSCAssert(NO, @"Received error in %@: %@", self, error);
// Log the error if we're running with assertions disabled.
NSLog(@"Received error in %@: %@", self, error);
}];
// Capture `self` weakly for the target's deallocation disposable, so we can
// freely deallocate if we complete before then.
@weakify(self);
[strongTarget.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
@strongify(self);
[self.leadingTerminal sendCompleted];
self.target = nil;
}]];
return self;
}
- 可以看出,初始化的时候,使用了
- (RACDisposable *)rac_observeKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)weakObserver block:(void (^)(id, NSDictionary *, BOOL, BOOL))block
监听了传入的 target 的 keyPath。但是最重要的是以下发送信息的部分:
RACDisposable *observationDisposable = [strongTarget rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent) {
// If the change wasn't triggered by deallocation, only affects the last
// path component, and ignoreNextUpdate is set, then it was triggered by
// this channel and should not be forwarded.
if (!causedByDealloc && affectedOnlyLastComponent && self.currentThreadData.ignoreNextUpdate) {
[self destroyCurrentThreadData];
return;
}
[self.leadingTerminal sendNext:value];
}];
接收到 target 的 keyPath 改变消息后,并不会都 sendNext
。而是先判断self.currentThreadData.ignoreNextUpdate
的值。如果为 true
会忽略sendNext
并销毁 self.currentThreadData。
-
初始化时,还订阅了 self.leadingTerminal 信号,当收到消息时,会先执行
[self createCurrentThreadData];
并设置self.currentThreadData.ignoreNextUpdate = YES;
再去设置 target 的属性值,从上面可知self.currentThreadData.ignoreNextUpdate = YES;
时不会调用sendNext
,因此不会构成无限循环。 -
最终效果
target 对象的a属性,如果收了订阅消息,则会设置ignoreNextUpdate为YES,然后设置a的值为新的值,这时候会触发a的KVO,但是由于ignoreNextUpdate为YES所以不会发出消息。如果手动改变了a的值,这时,会触发a的KVO,但是由于ignoreNextUpdate为NO,所以会发出消息。
RACChannelTo
上面提到了实现了双向绑定的各个类,那么如何实现真正的双向绑定呢,其实一句就可以:
RACChannelTo(self, a) = RACChannelTo(self, b);
展开宏定义:
[[RACKVOChannel alloc] initWithTarget:self keyPath:@"a" nilValue:nil][@"followingTerminal"] = [[RACKVOChannel alloc] initWithTarget:self keyPath:@"b" nilValue:nil][@"followingTerminal"]
接下来看源码RACKVOChannel (RACChannelTo) 的实现:
- (RACChannelTerminal *)objectForKeyedSubscript:(NSString *)key {
NSCParameterAssert(key != nil);
RACChannelTerminal *terminal = [self valueForKey:key];
NSCAssert([terminal isKindOfClass:RACChannelTerminal.class], @"Key \"%@\" does not identify a channel terminal", key);
return terminal;
}
- (void)setObject:(RACChannelTerminal *)otherTerminal forKeyedSubscript:(NSString *)key {
NSCParameterAssert(otherTerminal != nil);
RACChannelTerminal *selfTerminal = [self objectForKeyedSubscript:key];
[otherTerminal subscribe:selfTerminal];
[[selfTerminal skip:1] subscribe:otherTerminal];
}
可以看出 [[RACKVOChannel alloc] initWithTarget:self keyPath:@"a" nilValue:nil][@"followingTerminal"] = [[RACKVOChannel alloc] initWithTarget:self keyPath:@"b" nilValue:nil][@"followingTerminal"]
的效果就是实现两个 followingTerminal 的双向绑定。
RACChannel 扩展
RAC库对常用的组件都进行了 RACChannel 扩展,在 UIKit 中下面的组件都提供了使用 RACChannel 的接口,用来实现数据的双向绑定。
UIControl.png示例
1、viewModel 与UITextField 双向绑定。
RACChannelTo(self.viewModel, username) = self.usernameTextField.rac_newTextChannel;
2、属性双向绑定。
RACChannelTo(self, a) = RACChannelTo(self, b);
3、UITextField 双向绑定。
[self.textField.rac_newTextChannel subscribe:self.anotherTextField.rac_newTextChannel];
[self.anotherTextField.rac_newTextChannel subscribe:self.textField.rac_newTextChannel];
总结
虽然双向绑定原理稍微复杂一些,但是在使用的时候 ReactiveObjC 提供的API 已经足够简单了,非常方便我们实现视图与模型的双向绑定。