速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

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:55725

评论

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

联邦学习:对“数据隐私保护”和“数据孤岛”困境的破局

vivo互联网技术

人工智能 联邦学习 数据隐私 数据安全 gdpr

全套解决方案:中文NLP训练框架,支持大模型训练和文本生成,快速上手,海量训练数据!

汀丶人工智能

人工智能 自然语言处理 大语言模型

蓝易云:IT知识百科:什么是分布式云?

百度搜索:蓝易云

云计算 Linux 运维 云服务器 分布式云

R语言之 dplyr 包

timerring

R 语言

酷睿轻薄本也能运行大语言模型,英特尔推动 PC 生成式 AI 落地

E科讯

如何基于 Kubernetes 实现优质开发者平台体验?

SEAL安全

Kubernetes IdP 平台工程 内部开发者平台

资源成本降低70%!华为MetaERP资产核算的Serverless架构实践

轶天下事

首期华为云ROMA Connect《企业集成战略与华为数字化之道》高研班在东莞圆满举办

平平无奇爱好科技

软件开发必读!华为云软件开发生产线CodeArts深度体验指南

平平无奇爱好科技

代码随想录 Day51 - 动态规划(十二)

jjn0703

最新中文 Keka for Mac(压缩解压工具) v1.3.3

mac大玩家j

解压缩软件 解压软件 解压缩工具

一文了解JVM对象内存布具以及内存分配规则

java易二三

Java 程序员 JVM 计算机

窗口到底有多滑动?揭秘TCP/IP滑动窗口的工作原理

华为云开发者联盟

后端 开发 华为云 华为云开发者联盟 企业号 8 月 PK 榜

质效提升 | 聊聊QA与业务测试

laofo

DevOps 研发效能 持续交付 质量赋能

解决访问 Amazon S3 对象时遇到的“访问被拒绝”错误

亚马逊云科技 (Amazon Web Services)

存储

Presto 设计与实现(八):Presto JDBC

冰心的小屋

数据湖 JDBC presto 设计与实现 presto jdbc

OpenHarmony 4.0 Beta2新版本发布,邀您体验

OpenHarmony开发者

OpenHarmony

Forrester首次面向中国的开源报告:阿里云在云原生领域开源布局最全面

阿里巴巴云原生

阿里云 开源 云原生

SpringBoot 的优雅的接口参数验证

java易二三

Java 编程 程序员 计算机

Python案例|Matplotlib库实现的数据分析

TiAmo

Python 数据挖掘 数据分析

[小笔记] Java 线程池

java易二三

Java 程序员 线程 线程池 计算机

开创以API为核心的数字化变革,华为云实现API全生命周期一体化协作

平平无奇爱好科技

Wolfram Mathematica 13 中文激活+安装教程最新

胖墩儿不胖y

Mac软件 数学计算软件 计算工具

移动端应用程序的一些测试方案和策略

QE_LAB

移动端测试

鼎友餐饮信息总监杨山海:餐饮新增长依托数智应用,用数字化打造单店盈利模型

科创人

解锁多核处理器的力量:探索数据并行化在 Java 8 Stream 中的应用

java易二三

Java 程序员 计算机

多币种挖矿dapp流动性LP令牌质押开发搭建[源码部署]

V\TG【ch3nguang】

质押挖矿 流动性挖矿

火山引擎DataLeap基于Apache Atlas自研异步消息处理框架

字节跳动数据平台

数据中台 数据治理 数据安全 数据研发 企业号 8 月 PK 榜

蓝易云:如何在Linux系统服务器中测试存储/磁盘I/O性能?

百度搜索:蓝易云

Linux 运维 io 磁盘

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