写点什么

RAC 中的双向数据绑定 RACChannel

  • 2019-12-09
  • 本文字数:5097 字

    阅读完需:约 17 分钟

RAC 中的双向数据绑定 RACChannel

之前讲过了 ReactiveCocoa 中的一对一的单向数据流 RACSignal 和一对多的单向数据流 RACMulticastConnection,这一篇文章分析的是一对一的双向数据流 RACChannel



RACChannel 其实是一个相对比较复杂的类,但是,对其有一定了解之后合理运用的话,会在合适的业务中提供非常强大的支持能够极大的简化业务代码。

RACChannel 简介

RACChannel 可以被理解为一个双向的连接,这个连接的两端都是 RACSignal 实例,它们可以向彼此发送消息,如果我们在视图和模型之间通过 RACChannel 建立这样的连接:



那么从模型发出的消息,最后会发送到视图上;反之,用户对视图进行的操作最后也会体现在模型上。这种通信方式的实现是基于信号的,RACChannel 内部封装了两个 RACChannelTerminal 对象,它们都是 RACSignal 的子类:



对模型进行的操作最后都会发送给 leadingTerminal 再通过内部的实现发送给 followingTerminal,由于视图是 followingTerminal 的订阅者,所以消息最终会发送到视图上。



在上述情况下,leadingTerminal 的订阅者(模型)并不会收到消息,它的订阅者(视图)只会在 followingTerminal 收到消息时才会接受到新的值。


同时,RACChannel 的绑定都是双向的,视图收到用户的动作,例如点击等事件时,会将消息发送给 followingTerminal,而 followingTerminal不会将消息发送给自己的订阅者(视图),而是会发送给 leadingTerminal,并通过 leadingTerminal 发送给其订阅者,即模型。



上图描述了信息在 RACChannel 之间的传递过程,无论是模型属性的改变还是用户对视图进行的操作都会通过这两个 RACChannelTerminal 传递到另一端;同时,由于消息不会发送给自己的订阅者,所以不会造成信息的循环发送。

RACChannel 和 RACChannelTerminal

RACChannelRACChannelTerminal 的关系非常密切,前者可以理解为一个网络连接,后者可以理解为 socket,表示网络连接的一端,下图描述了 RACChannel 与网络连接中概念的一一对应关系。



  • 在客户端使用 writesocket 中发送消息时,socket 的持有者客户端不会收到消息,只有在 socket 上调用 read 的服务端才会收到消息;反之亦然。

  • 在模型使用 sendNextleadingTerminal 中发送消息时,leadingTerminal 的订阅者模型不会收到消息,只有在 followingTerminal 上调用 subscribe 的视图才会收到消息;反之亦然。

RACChannelTerminal 的实现

为什么向 RACChannelTerminal 发送消息,它的订阅者获取不到?先来看一下它在头文件中的定义:


Objective-C


@interface RACChannelTerminal : RACSignal <RACSubscriber>@end
复制代码


RACChannelTerminal 是一个信号的子类,同时它还遵循了 RACSubscriber 协议,也就是可以向它调用 -sendNext: 等方法;RAChannelTerminal 中持有了两个对象:



在初始化时,需要传入 valuesotherTerminal 这两个属性,其中 values 表示当前断点,otherTerminal 表示远程端点:


Objective-C


- (instancetype)initWithValues:(RACSignal *)values otherTerminal:(id<RACSubscriber>)otherTerminal {  self = [super init];  _values = values;  _otherTerminal = otherTerminal;  return self;}
复制代码


当然,作为 RACSignal 的子类,RACChannelTerminal 必须覆写 -subscribe: 方法:


Objective-C


- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {  return [self.values subscribe:subscriber];}
复制代码


在订阅者调用 -subscribeNext: 等方法发起订阅时,实际上订阅的是当前端点;如果向当前端点发送消息,会被转发到远程端点上,而这也就是当前端点的订阅者不会接收到向当前端点发送消息的原因:


Objective-C


- (void)sendNext:(id)value {  [self.otherTerminal sendNext:value];}- (void)sendError:(NSError *)error {  [self.otherTerminal sendError:error];}- (void)sendCompleted {  [self.otherTerminal sendCompleted];}
复制代码

RACChannel 的初始化

我们在任何情况下都不应该直接使用 -init 方法初始化 RACChannelTerminal 的实例,而是应该以创建 RACChannel 的方式生成它:


Objective-C


- (instancetype)init {  self = [super init];
RACReplaySubject *leadingSubject = [RACReplaySubject replaySubjectWithCapacity:0]; RACReplaySubject *followingSubject = [RACReplaySubject replaySubjectWithCapacity:1];
[[leadingSubject ignoreValues] subscribe:followingSubject]; [[followingSubject ignoreValues] subscribe:leadingSubject];
_leadingTerminal = [[RACChannelTerminal alloc] initWithValues:leadingSubject otherTerminal:followingSubject]; _followingTerminal = [[RACChannelTerminal alloc] initWithValues:followingSubject otherTerminal:leadingSubject];
return self;}
复制代码


两个 RACChannelTerminal 中包装的其实是两个 RACSubject 热信号,它们既可以作为订阅者,也可以接收其他对象发送的消息;我们并不希望 leadingSubject 有任何的初始值,但是我们需要 errorcompleted 信息可以被重播。



通过 -ignoreValues-subscribe: 方法,leadingSubjectfollowingSubject 两个热信号中产生的错误会互相发送,这是为了防止连接的两端一边发生了错误,另一边还继续工作的情况的出现。


在初始化方法的最后,生成两个 RACChannelTerminal 实例的过程就不多说了。

RACChannel 与 UIKit 组件

如果在整个 ReactiveCocoa 工程中搜索 RACChannel,你会发现以下的 UIKit 组件都与 RACChannel 有着非常密切的关系:



UIKit 中的这些组件都提供了使用 RACChannel 的接口,用以降低数据双向绑定的复杂度,我们以 UITextField 为例,它在分类的接口中提供了 rac_newTextChannel 方法:


Objective-C


- (RACChannelTerminal *)rac_newTextChannel {  return [self rac_channelForControlEvents:UIControlEventAllEditingEvents key:@keypath(self.text) nilValue:@""];}
复制代码


上述方法用于返回一个一端绑定 UIControlEventAllEditingEvents 事件的 RACChannelTerminal 对象。



UIControlEventAllEditingEvents 事件发生时,它会将自己的 text 属性作为信号发送到 followingTerminal -> leadingTerminal 管道中,最后发送给 leadingTerminal 的订阅者。


rac_newTextChannel 中调用的方法 -rac_channelForControlEvents:key:nilValue: 是一个 UIControl 的私有方法:


Objective-C


- (RACChannelTerminal *)rac_channelForControlEvents:(UIControlEvents)controlEvents key:(NSString *)key nilValue:(id)nilValue {  key = [key copy];  RACChannel *channel = [[RACChannel alloc] init];
RACSignal *eventSignal = [[[self rac_signalForControlEvents:controlEvents] mapReplace:key] takeUntil:[[channel.followingTerminal ignoreValues] catchTo:RACSignal.empty]]; [[self rac_liftSelector:@selector(valueForKey:) withSignals:eventSignal, nil] subscribe:channel.followingTerminal];
RACSignal *valuesSignal = [channel.followingTerminal map:^(id value) { return value ?: nilValue; }]; [self rac_liftSelector:@selector(setValue:forKey:) withSignals:valuesSignal, [RACSignal return:key], nil];
return channel.leadingTerminal;}
复制代码


这个方法为所有的 UIControl 子类,包括 UITextFieldUISegmentedControl 等等,它的主要作用就是当传入的 controlEvents 事件发生时,将 UIKit 组件的属性 key 发送到返回的 RACChannelTerminal 实例中;同时,在向返回的 RACChannelTerminal 实例中发送消息时,也会自动更新 UIKit 组件的属性。


上面的代码在初始化 RACChannel 之后做了两件事情,首先是在 UIControlEventAllEditingEvents 事件发生时,将 text 属性发送到 followingTerminal 中:


Objective-C


RACSignal *eventSignal = [[[self    rac_signalForControlEvents:controlEvents]    mapReplace:key]    takeUntil:[[channel.followingTerminal        ignoreValues]        catchTo:RACSignal.empty]];[[self    rac_liftSelector:@selector(valueForKey:) withSignals:eventSignal, nil]    subscribe:channel.followingTerminal];
复制代码


第二个是在 followingTerminal 接收到来自 leadingTerminal 的消息时,更新 UITextFieldtext 属性。


Objective-C


RACSignal *valuesSignal = [channel.followingTerminal    map:^(id value) {        return value ?: nilValue;    }];[self rac_liftSelector:@selector(setValue:forKey:) withSignals:valuesSignal, [RACSignal return:key], nil];
复制代码


这两件事情都是通过 -rac_liftSelector:withSignals: 方法来完成的,不过,我们不会在这篇文章中介绍这个方法。

RACChannel 与 KVO

RACChannel 不仅为 UIKit 组件提供了接口,还为键值观测提供了 RACKVOChannel 来高效地完成双向绑定;RACKVOChannelRACChannel 的子类:



RACKVOChannel 提供的接口中,我们一般都会使用 RACChannelTo 来观测某一个对象的对应属性,三个参数依次为对象、属性和默认值:


Objective-C


RACChannelTerminal *integerChannel = RACChannelTo(self, integerProperty, @42);
复制代码


RACChannelToRACKVOChannel 头文件中的一个宏,上面的表达式可以展开成为:


Objective-C


RACChannelTerminal *integerChannel = [[RACKVOChannel alloc] initWithTarget:self keyPath:@"integerProperty" nilValue:@42][@"followingTerminal"];
复制代码


该宏初始化了一个 RACKVOChannel 对象,并通过方括号的方式获取其中的 followingTerminal,这种获取类属性的方式是通过覆写以下的两个方法实现的:


Objective-C


- (RACChannelTerminal *)objectForKeyedSubscript:(NSString *)key {  RACChannelTerminal *terminal = [self valueForKey:key];  return terminal;}
- (void)setObject:(RACChannelTerminal *)otherTerminal forKeyedSubscript:(NSString *)key { RACChannelTerminal *selfTerminal = [self objectForKeyedSubscript:key]; [otherTerminal subscribe:selfTerminal]; [[selfTerminal skip:1] subscribe:otherTerminal];}
复制代码


又由于覆写了这两个方法,在 -setObject:forKeyedSubscript: 时会自动调用 -subscribe: 方法完成双向绑定,所以我们可以使用 = 来对两个 RACKVOChannel 进行双向绑定:


Objective-C


RACChannelTo(view, property) = RACChannelTo(model, property);
[[RACKVOChannel alloc] initWithTarget:view keyPath:@"property" nilValue:nil][@"followingTerminal"] = [[RACKVOChannel alloc] initWithTarget:model keyPath:@"property" nilValue:nil][@"followingTerminal"];
复制代码


以上的两种方式是完全等价的,它们都会在对方的属性更新时更新自己的属性。



实现的方式其实与 RACChannel 差不多,这里不会深入到代码中进行介绍,与 RACChannel 的区别是,RACKVOChannel 并没有暴露出 leadingTerminal 而是 followingTerminal


RACChannel 实战

这一小节通过一个简单的例子来解释如何使用 RACChannel 进行双向数据绑定。



在整个视图上有两个 UITextField,我们想让这两个 UITextField text 的值相互绑定,在一个 UITextField 编辑时也改变另一个 UITextField 中的内容:


Objective-C


@property (weak, nonatomic) IBOutlet UITextField *textField;@property (weak, nonatomic) IBOutlet UITextField *anotherTextField;
复制代码


实现的过程非常简单,分别获取两个 UITextFieldrac_newTextChannel 属性,并让它们订阅彼此的内容:


Objective-C


[self.textField.rac_newTextChannel subscribe:self.anotherTextField.rac_newTextChannel];[self.anotherTextField.rac_newTextChannel subscribe:self.textField.rac_newTextChannel];
复制代码


这样在使用两个文本输入框时就能达到预期的效果了,这是一个非常简单的例子,可以得到如下的结构图。



两个 UITextField 通过 RACChannel 互相影响,在对方属性更新时同时更新自己的属性。

总结

RACChannel 非常适合于视图和模型之间的双向绑定,在对方的属性或者状态更新时及时通知自己,达到预期的效果;我们可以使用 ReactiveCocoa 中内置的很多与 RACChannel 有关的方法,来获取开箱即用的 RACChannelTerminal,当然也可以使用 RACChannelTo 通过 RACKVOChannel 来快速绑定类与类的属性。

References


Github Repo:iOS-Source-Code-Analyze


Source: https://draveness.me/racchannel


本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/racchannel


2019-12-09 15:55739

评论

发布
暂无评论
发现更多内容

详解KubeEdge EdgeMesh v1.15 边缘CNI特性

华为云开发者联盟

开发 华为云 容器网络 华为云开发者联盟

Project项目管理软件好用吗?实用性分析

爱吃小舅的鱼

开源 项目管理 项目管理软件

StarRocks 生成列:百倍提速半结构化数据分析

StarRocks

数据库 StarRocks 半结构化数据分析

鸿蒙Next来了,企业应该如何快速应对

Onegun

鸿蒙 鸿蒙系统

百度基于金融场景构建高实时、高可用的分布式数据传输系统的技术实践

JackJiang

网络编程 即时通讯 IM

Web Components从技术解析到生态应用个人心得指北

zhoulujun

QCN9024 and QCN9074: Wallys DR9074 Triband Module ATH11k QSDK Supported

wallyslilly

QCN9074 QCN9024

智能开发助手——华为云CodeArts Snap,揭开智能研发新篇章

华为云PaaS服务小智

人工智能 华为云

Permute 3 for mac(全能媒体格式转换器)v3.11.4中文版下载

iMac小白

阿里云容器服务助力万兴科技 AIGC 应用加速

阿里巴巴云原生

阿里云 云原生 容器服务

手机崩溃日志的查找与分析

雪奈椰子

Premiere Pro 2024 for Mac(PR 2024视频编辑软件)v24.1中文激活版

iMac小白

Spring Boot 整合 Camunda 实现工作流

EquatorCoco

Java 后端 开发 springboot

克服传统企业数字化转型的十大阻力

天津汇柏科技有限公司

数字化转型

【论文解读】用于代码处理的语言模型综述

合合技术团队

代码 自然语言模型 大语言模型 文献综述

百度搜索Push个性化:新的突破

百度Geek说

推荐算法 百度搜索 搜索push

万界星空科技注塑行业生产管理MES系统管理

万界星空科技

mes 万界星空科技 注塑制造业 注塑MES 注塑生产管理系统

Mac无损音乐播放器 Audirvana中文激活版下载

iMac小白

国外服务器租用:如何在预算内选择最优服务

一只扑棱蛾子

国外服务器

大家都在用哪些开源项目系统?20款推荐

爱吃小舅的鱼

开源 项目管理 项目管理工具

​食品饮料行业如何进行数字化转型?

万界星空科技

数字化转型 mes 万界星空科技 食品饮料行业数字化转型 食品MES

泛互联网行业A/B测试全解析:产品优化的创新之道

字节跳动数据平台

A/B 测试

RAC 中的双向数据绑定 RACChannel_语言 & 开发_Draveness_InfoQ精选文章