RAC双向绑定

简介

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容