实用工具iOS 响应式编程原理篇

RAC双向绑定

2017-11-18  本文已影响241人  秦明Qinmin

简介

在 ReactiveObjC 中,根据数据流的方向,我们可以划分出两种不同数据流,即:单向数据流,如:RACSignal、RACSubject、RACMulticastConnection;双向数据流,如:RACChannel、RACKVOChannel。这篇文章主要介绍 ReactiveObjC 中的双向数据流。当我们需要实现数据的双向绑定时(A的改动影响B,B的改动也影响A),使用 ReactiveObjC 提供的双向数据流可以很方便的实现相关需求。

RACChannel

RACChannel 类似一个双向连接,连接的两端都是 RACSignal 实例。RACChannel 像一个魔法盒子,我们可以在A端发送信号,在B端订阅A端的信号,也可以在B端发送信号,在A端订阅B端的信号。如下所示:

RACChannel.png
1、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 发送的消息。原理图如下:

RACChannelTerminal.png
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.png

RACChannel 内部的双箭头表示这两个 RACReplaySubject 为同一个热信号,由于只创建了两个 RACReplaySubject 热信号,因此,在两个 RACChannelTerminal 中,只是交换了_values 和 _otherTerminal 的位置。

双向绑定

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 *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。

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 的双向绑定。

RACChannelTo.png

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 已经足够简单了,非常方便我们实现视图与模型的双向绑定。

上一篇下一篇

猜你喜欢

热点阅读