初探ReactiveCocoa心灵感受
基础理论
函数响应型编程
FRP是由两种概念组合而来的:
- 响应型编程(Reactive Programming),它关注的是异步数据流,你需要监听并根据其中的数据做出响应。
- 函数式编程(Functional Programming),它的函数定义具有数学风格,计算过程中尽量避免使用变量和状态值,代码更加灵活无副作用。
函数响应型编程实例
case 1
想象有一款应用,需要关注用户的位置变化,并且在发现他的位置靠近一个咖啡店的时候提示他。
通过FRP方式需要这样实现:
- 创建一个对象,它发出你需要响应的位置变化事件的数据流
- 通过筛选这些位置信息,把那些靠近咖啡店的位置信息显示出来
locationProducer . filter ( ifLocationNearCoffeeShops ) . startWithNext {[ weak self ] location in self ?. alertUser ( location ) } |
locationProducer
在位置信息每次发生变化的时候,都会抛出一个事件(event)。ReactiveCocoa 把它称作signal
,RxSwift 中称作sequence
。- 然后利用函数式编程(Functional Programming)技术去处理这些变化事件的数据。
filter
函数的用法和数组(array)中的相同,将数据流中的每个值作为参数传给ifLocationNearCoffeeShops
处理,如果返回true
,这个事件会继续传递到下一步。
- 最后,
startWithNext
就是一个订阅方法,当有(过滤过的)事件传递到这里的时候,你传入的闭包表达式将会执行,并将那些数据作为参数提供给你。
注:上面的代码是异步的,那个过滤方法和闭包表达式只有在位置变化事件发生的时候才会被执行。这体现了函数式编程的精髓,又十分贴切时间轴上的数据的概念。你需要关心的是数据来了,而不是产生这些数据的细节。
case 2 事件转换
上面的例子中,仅仅是关注了位置变化的数据流,除了过滤一些靠近咖啡店的位置,并没有对这些事件做更多的处理。 而FRP范式另一个基本要素,能够将这些事件数据进行组合转换,使其变得更有意义一些 ,可以利用一些高阶函数实现。(map,filter,reduce,combine,zip)
代码优化,过滤掉那些重复的位置信息,并且将传过来的位置数据(CLLocation)转换成一段用户可识别的文本信息--
locationProducer . skipRepeats () // 1 . filter ( ifLocationNearCoffeeShops ) . map ( toHumanReadableLocation ) // 2 . startWithNext {[ weak self ] readableLocation in self ?. alertUser ( readableLocation ) } |
- 首先在
locationProducer
发出数据流之后,加一步skipRepeats
操作,这个操作并不是array
所具有的;它是 ReactiveCocoa 所特有的。这个方法的意图很明显:过滤掉那些相等的数据事件(这些事件的数据需要具有可比性)。 - 在
filter
方法执行过后,map
函数的作用就是将一种事件数据转换成另一种,比如把CLLocation
类型转换成String
类型。
信号
RAC的核心是信号,即RACSignal。信号可以看做是传递数据的工具,当数据变化时,信号就会发送改变的信息,以通知信号的订阅者执行方法。
冷热信号
- Hot Observable 是主动的,尽管你没有订阅事件,但是它会时刻推送,就像鼠标移动。Cold Observable是被动的,只有当你订阅的时候,它才会发布消息。
- Hot Observable可以有多个订阅者,是一对多,集合可以与订阅者共享信息。而Cold Observable只能一对一,当有不同的订阅者,消息是重新完整发送。
- RACSubject及其子类是热信号。RACSignal排除RACSubject类以外的是冷信号。
想象一下,你需要发起一个网络请求,并且解析它的响应数据,然后展示给用户--
let requestFlow = networkRequest . flatMap ( parseResponse ) requestFlow . startWithNext {[ weak self ] result in self ?. showResult ( result ) } |
只有当你订阅一个signal(也就是进行startWithNext操作)的时候,网络请求才会被创建并发起。这种signal被称作是冷信号,因为直到你订阅它们之前,它们是出于“冻结”状态的
相反,当订阅一个热信号时,它就已经开始了,所以你可以观察到第三或者第四次事件,或者更多。比较典型的例子就是敲击键盘产生的事件流。
总结一下
- 一个冷信号是指,当你想要订阅他的时候,需要执行开始任务,每个新的订阅者都需要执行开始任务,订阅
requestFlow
三次也就意味着相对应的要创建三个网路请求。 - 一个热信号创建时就已经可以发送事件了,订阅者不需要去开启它。通常 UI 交互是属于热信号。
- ReactiveCocoa 针对热、冷信号分别提供了这两种类型:Signal<T,E> 与 SignalProducer<T,E> 。而RXSwift提供了一种同时支持冷、冷信号的类型:Observable<T>
Q: 为什么要区分冷热信号
A:知道一个信号的含义是很重要的,因为它更好的描述了如何在一个特定的上下文中运用它。当处理一个复杂的系统时,这些将会有很大不同。 假如你正在处理一个热信号,由于某种原因它变成了冷信号,这个时候你将会对每个订阅者进行副作用编程,这将会给你的应用带来很大影响。 实例是,你的应用中,有三个或四个实体需要监听同一种网络请求,而对于每个新的订阅,都会发起一个新的网络请求。
事件类型
这两个框架中,主要有三种事件类型:
Next<T>
:每当一个新的值(T
类型)被传到事件流中时,这种事件就会被触发。在上面跟踪定位的那个例子中,T
指的就是CLLocation
。Compleled
:表示事件流的终止。收到这个事件之后,将不会在发送Next<T>
和Error<E>
。
Error<E>
:表示一个错误。在服务器请求的例子中,当你收到一个服务器错误时,这个事件将会被发送。E
是遵循了ErrorType
协议的错误类型。收到这个事件之后,将不会在发送Next<T>
和Compleled
。
注:
ReactiveCocoa 的 Singal<T, E>
和 SignalProducer<T, E>
有两个参数类型,而 RxSwift 的 Observable<T>
只有一个。前者的第二个类型(E
)是遵循了 ErrorType
协议的子类型。在 RxSwift 中这个类型被删除了,取而代之的是一个需要内部处理的 ErrorType
协议类型。
常用类
- RACSignal
信号类,只有当数据变化时,才会发送数据。但是RACSignal自己不具备发送信号能力,而是交给订阅者去发送。默认一个信号发送数据完毕就会自动取消订阅 ,但如果订阅者还在,就不会自动取消信号订阅,因此,如果在实际开发中 需要自己控制订阅者的生命周期 ,可以strong持有,在特定的时机执行dispose方法取消订阅。
RACSignal订阅和发送信号一般过程如下:
- 创建信号 createSignal
RACSignal *single = [RACSignal createSignal:^RACDisposable *(idsubscriber) {}]
- 创建订阅者进行订阅
[single subscribeNext:]
- 发送信号
[subscriber sendNext:]
- RACSubscriber
订阅者,它不是一个类而是一个协议,实现这个协议的类都称为订阅者
- RACDisposable
执行订阅取消或者进行对资源的清理工作,dispose
- RACSubject
是一个继承RACSignal并且遵守RACSubscriber协议的类。所以这一个类不仅可以处理信号,还可以发送信 号。因为RACSubject的subscribeNext方法内部有数组subscribers,可以保存所有的订阅者,而 RACSubject的sendNext在发送信号的时候会遍历所有的订阅者,订阅者执行nextBlock。
- RACReplaySubject
继承RACSubject,和RACSubject不同之处在于 RACReplaySubject可以先发送信号,然后再订阅信号 。原 因在于RACReplaySubject的subscribe方法中遍历所有的订阅者,拿到当前订阅者发送数据。 RACReplaySubject的sendNext方法是先保存值,然后再发送数据,RACSubject则是直接遍历发送数据。
- RACMulticastConnection
连接类是为了当我们多次订阅同一个信号的时候,避免订阅信号的block中的代码被调用多次。
- RACCommand
这个类负责处理事件,可以控制事件的传递以及数据的传递,监控事件的执行过程。
入门语法
UITextField输入文本监听
- 假如有一个UITExtField 文本框,需要对其输入内容进行监听
accountTextField . reactive . continuousTextValues . observeValues { ( text ) in print ( text ?? "" ) } |
- 如果你不想知道TextField文本内容,而是想知道文本长度,可以 使用map函数进行信号内容的修改 ,然后再对map后的信号进行观察。map函数可以对信号的内容进行转换,它的返回值可以是任何你想要的类型。
accountTextField . reactive . continuousTextValues . map { ( text ) - > Int in return text !. characters . count }. observeValues { ( count ) in print ( count ) } |
- 也许你只是想在某个条件下,才想监听文本内容,比如当文本的内容大于3的时候。这时可以使用filter函数进行过滤操作。filter函数进行过滤操作。filter函数只返回Bool类型。 只有当filter函数返回true的时候,信号继续传递。我们才能监听到文本的内容 ,当filter函数返回false的时候,信号会被拦截。
accountTextField . reactive . continuousTextValues . filter { ( text ) - > Bool in return text !. characters . count > 3 }. observeValues { ( text ) in print ( text ?? "" ) } |
UIButton 点击事件监听
loginBtn . reactive . controlEvents (. touchUpInside ). observeValues { ( btn ) in print ( btn . currentTitle ) } |
Combine Signal 混合信号
let accountSignal = accountTextField . reactive . continuousTextValues . map { ( text ) - > Bool in return text !. count > 3 } let pwdSignal = pwdTextField . reactive . continuousTextValues . map { ( text ) - > Bool in return text !. characters . count > 3 } |
// 使用combineLatest函数将两个信号组合为一个信号,然后利用map函数对信号进行转换,使用 <~将信号的结果绑定到loginBtn loginBtn . reactive . isEnabled < ~ accountSignal . combineLatest ( with : pwdSignal ). map ({ $ 0 & & $ 1 }) //此方法等价于-- loginBtn . reactive . isEnabled < ~ accountSignal . combineLatest ( with : pwdSignal ). map ({ ( accountValid , pwdValid ) - > Bool in return accountValid & & pwdValid }) |
MutableProperty
此方法用于对某个属性的value 进行观察---
//<>里面可以是任意类型,它代表属性的类型。 let racValue = MutableProperty < Int > ( 1 ) racValue . producer . startWithValues { ( make ) in print ( make ) } racValue . value = 10 |
方法调用拦截
当你想获取到某个方法被调用的事件,比如UIViewController的ViewWillAppear事件
self . reactive . trigger ( for : # selector ( UIViewController . viewWillAppear ( _ :))). observeValues { () in print ( "viewWillAppear被调用了" ) } |
监听对象的生命周期
如果你想在某个对象被销毁以后,做一些事情。那么你可以对这个对象的生命周期进行监听,也就是当对象销毁的时候,你获得对象销毁的信号,然后观察这个信号。当然你也可以重写deinit函数。
var textF : UITextField ? override func viewDidLoad () { super . viewDidLoad () textF = UITextField () print ( textF !) textF !. reactive . lifetime . ended . observeCompleted { print ( "销毁" ) } textF = UITextField () print ( textF !) } /* <UITextField: 0x7fded1014800; frame = (0 0; 0 0); text = ''; opaque = NO; layer = <CALayer: 0x600002ceb3c0>> <UITextField: 0x7fded1011000; frame = (0 0; 0 0); text = ''; opaque = NO; layer = <CALayer: 0x600002ceb620>> 销毁 */ |
开始的时候给textF属性赋值一个对象,然后对该对象的生命周期进行监听。然后给textF属性重新赋值一个另一个内存地址对象。因为textF指针指向了别处,所以这个时候原先内存的引用计数变为0,所以原先的对象会被销毁,而我们正好对原先内存对象的生命周期进行了监听,所以会获取对象被销毁事件。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了