RAC雙向綁定

簡(jiǎn)介

在 ReactiveObjC 中,根據(jù)數(shù)據(jù)流的方向,我們可以劃分出兩種不同數(shù)據(jù)流,即:?jiǎn)蜗驍?shù)據(jù)流,如:RACSignal、RACSubject、RACMulticastConnection;雙向數(shù)據(jù)流,如:RACChannel、RACKVOChannel。這篇文章主要介紹 ReactiveObjC 中的雙向數(shù)據(jù)流。當(dāng)我們需要實(shí)現(xiàn)數(shù)據(jù)的雙向綁定時(shí)(A的改動(dòng)影響B(tài),B的改動(dòng)也影響A),使用 ReactiveObjC 提供的雙向數(shù)據(jù)流可以很方便的實(shí)現(xiàn)相關(guān)需求。

RACChannel

RACChannel 類似一個(gè)雙向連接,連接的兩端都是 RACSignal 實(shí)例。RACChannel 像一個(gè)魔法盒子,我們可以在A端發(fā)送信號(hào),在B端訂閱A端的信號(hào),也可以在B端發(fā)送信號(hào),在A端訂閱B端的信號(hào)。如下所示:

RACChannel.png
1、RACChannelTerminal

在看 RACChannel 的源碼之前,我們需要先了解 RACChannelTerminal 這個(gè)類。之所以需要了解它,是因?yàn)?RACChannel 有兩個(gè)重要屬性 leadingTerminal、followingTerminal,它們分別代表了 RACChannel 的兩端,是實(shí)現(xiàn) RACChannel 的關(guān)鍵,而這兩個(gè)屬性都是 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 ,說明它可以被訂閱,同時(shí)實(shí)現(xiàn)了 RACSubscriber 協(xié)議,說明它可以發(fā)送消息。接下來(lái)看看RACChannelTerminal 的具體實(shí)現(xiàn):

RACChannelTerminal 實(shí)現(xiàn):

@implementation RACChannelTerminal

#pragma mark Lifecycle

- (instancetype)initWithValues:(RACSignal *)values otherTerminal:(id<RACSubscriber>)otherTerminal {
    NSCParameterAssert(values != nil);
    NSCParameterAssert(otherTerminal != nil);

    self = [super init];
        
    // 初始化兩個(gè)端點(diǎn)屬性
    _values = values;
    _otherTerminal = otherTerminal;

    return self;
}

#pragma mark RACSignal

// 訂閱時(shí),實(shí)際上被訂閱的是self.values信號(hào)
- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    return [self.values subscribe:subscriber];
}

#pragma mark <RACSubscriber>

// 發(fā)送時(shí),實(shí)際上是用self.otherTerminal 來(lái)發(fā)送消息
- (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

在初始化時(shí),RACChannelTerminal 需要傳入 values 和 otherTerminal 兩個(gè)值,其中 values、otherTerminal 分別表示 RACChannelTerminal 的兩個(gè)端點(diǎn)。在訂閱者調(diào)用 -subscribeNext: 等方法發(fā)起訂閱時(shí),實(shí)際上訂閱的是self.values 信號(hào);如果向當(dāng)前端點(diǎn)發(fā)送消息,會(huì)使用 self.otherTerminal 來(lái)發(fā)送消息。由于不是使用 self.values 的訂閱者來(lái)發(fā)送消息,因此,self.values 也就收不到 RACChannelTerminal 發(fā)送的消息。原理圖如下:

RACChannelTerminal.png
2、RACChannel

了解了 RACChannelTerminal 之后,我們?cè)賮?lái)看 RACChannel 的實(shí)現(xiàn),從源碼可以看出 RACChannel 有兩個(gè)屬性leadingTerminal、followingTerminal,他們分別代表了 RACChannel 的兩端,這兩個(gè)屬性都是 RACChannelTerminal 類型。

@interface RACChannel<ValueType> : NSObject
@property (nonatomic, strong, readonly) RACChannelTerminal<ValueType> *leadingTerminal;
@property (nonatomic, strong, readonly) RACChannelTerminal<ValueType> *followingTerminal;
@end

接下來(lái),我們看 RACChannel 的具體實(shí)現(xiàn):

- (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 初始化的時(shí)候,實(shí)際上只創(chuàng)建了兩個(gè) RACReplaySubject 熱信號(hào)。初始化 _leadingTerminal 和 _followingTerminal 兩個(gè)屬性時(shí),只是交換了兩個(gè) RACReplaySubject 的順序,因?yàn)閮?RACReplaySubject 是熱信號(hào),它們既可以作為訂閱者,也可以接收其他對(duì)象發(fā)送的消息。通過 -ignoreValues-subscribe: 方法,leadingSubject 和 followingSubject 兩個(gè)熱信號(hào)中產(chǎn)生的錯(cuò)誤會(huì)互相發(fā)送,目的是為了防止一邊發(fā)生了錯(cuò)誤,另一邊還繼續(xù)工作。原理圖如下:

RACChannel.png

RACChannel 內(nèi)部的雙箭頭表示這兩個(gè) RACReplaySubject 為同一個(gè)熱信號(hào),由于只創(chuàng)建了兩個(gè) RACReplaySubject 熱信號(hào),因此,在兩個(gè) RACChannelTerminal 中,只是交換了_values 和 _otherTerminal 的位置。

雙向綁定

  • 使用 RACChannel 實(shí)現(xiàn)雙向綁定。我們需要在 _leadingTerminal 端和 _followingTerminal 端分別實(shí)現(xiàn)訂閱和發(fā)送。
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];

不過遺憾的是會(huì)出現(xiàn)堆棧溢出的錯(cuò)誤,為什么呢?因?yàn)?RACChannel 只是實(shí)現(xiàn)了雙向綁定,并沒有幫我們處理循環(huán)調(diào)用的問題。在這里A的改動(dòng)會(huì)影響B(tài),B的改動(dòng)也會(huì)影響A,就這樣無(wú)限循環(huán)下去。

RACKVOChannel

直接使用 RACChannel,可能會(huì)出現(xiàn)堆棧溢出的錯(cuò)誤。因此,我們需要打斷這種死循環(huán)。這時(shí)候,我們就需要使用 RACKVOChannel 來(lái)實(shí)現(xiàn)雙向綁定了。RACKVOChannel 繼承自 RACChannel。接下來(lái)看一下它的初始化:

- (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;
}

  • 可以看出,初始化的時(shí)候,使用了- (RACDisposable *)rac_observeKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)weakObserver block:(void (^)(id, NSDictionary *, BOOL, BOOL))block 監(jiān)聽了傳入的 target 的 keyPath。但是最重要的是以下發(fā)送信息的部分:
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 改變消息后,并不會(huì)都 sendNext。而是先判斷self.currentThreadData.ignoreNextUpdate的值。如果為 true 會(huì)忽略sendNext并銷毀 self.currentThreadData。

  • 初始化時(shí),還訂閱了 self.leadingTerminal 信號(hào),當(dāng)收到消息時(shí),會(huì)先執(zhí)行 [self createCurrentThreadData]; 并設(shè)置self.currentThreadData.ignoreNextUpdate = YES;再去設(shè)置 target 的屬性值,從上面可知 self.currentThreadData.ignoreNextUpdate = YES; 時(shí)不會(huì)調(diào)用sendNext,因此不會(huì)構(gòu)成無(wú)限循環(huán)。

  • 最終效果
    target 對(duì)象的a屬性,如果收了訂閱消息,則會(huì)設(shè)置ignoreNextUpdate為YES,然后設(shè)置a的值為新的值,這時(shí)候會(huì)觸發(fā)a的KVO,但是由于ignoreNextUpdate為YES所以不會(huì)發(fā)出消息。如果手動(dòng)改變了a的值,這時(shí),會(huì)觸發(fā)a的KVO,但是由于ignoreNextUpdate為NO,所以會(huì)發(fā)出消息。

RACChannelTo

上面提到了實(shí)現(xiàn)了雙向綁定的各個(gè)類,那么如何實(shí)現(xiàn)真正的雙向綁定呢,其實(shí)一句就可以:

RACChannelTo(self, a) = RACChannelTo(self, b);

展開宏定義:

[[RACKVOChannel alloc] initWithTarget:self keyPath:@"a" nilValue:nil][@"followingTerminal"] = [[RACKVOChannel alloc] initWithTarget:self keyPath:@"b" nilValue:nil][@"followingTerminal"]

接下來(lái)看源碼RACKVOChannel (RACChannelTo) 的實(shí)現(xiàn):

- (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"]的效果就是實(shí)現(xiàn)兩個(gè) followingTerminal 的雙向綁定。

RACChannelTo.png

RACChannel 擴(kuò)展

RAC庫(kù)對(duì)常用的組件都進(jìn)行了 RACChannel 擴(kuò)展,在 UIKit 中下面的組件都提供了使用 RACChannel 的接口,用來(lái)實(shí)現(xiàn)數(shù)據(jù)的雙向綁定。

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];

總結(jié)

雖然雙向綁定原理稍微復(fù)雜一些,但是在使用的時(shí)候 ReactiveObjC 提供的API 已經(jīng)足夠簡(jiǎn)單了,非常方便我們實(shí)現(xiàn)視圖與模型的雙向綁定。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容