RAC双向绑定

作者: 秦明Qinmin | 来源:发表于2017-11-18 21:30 被阅读241次

    简介

    在 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 实现双向绑定。我们需要在 _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 的双向绑定。

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

    相关文章

      网友评论

        本文标题:RAC双向绑定

        本文链接:https://www.haomeiwen.com/subject/bjrevxtx.html