ReactiveCocoa 解决了什么问题
ReactiveCocoa 是一个 iOS 中的函数式响应式编程框架,它改变了我们在使用 Cocoa 时的思维和方式。
它将苹果的 API 进行了一次封装改造,使其可以使用响应式进行编程。
函数式编程 (Functional Programming) [FP]
注: 本人之前并不了解函数式编程,也是为了深入了解 ReactiveCocoa 为什么会那样写代码才去研究的,了解也比较浅显。有需要了解的同学可以看看这篇文章 : 函数式编程(Functional Programming)简介
在 iOS 中,开发者一般使用的开发范例都是面向对象开发,而面向对象是一种命令式编程 (Imperative Programming)。它与函数式编程是两种开发思维,其实目的都是一样的,让计算机计算出来我们需要的结果。
In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
在计算机科学中,函数式编程是一种指定了计算机编程中编程结构和元素的编程范例,将一次计算作为一个数学函数,避免了状态的改变并且不会改变原始数据。
总的来说,函数式编程就是一个表达式,y = x + 1, 就像数学中的函数一样。
比如需要求 1 到 n 的和,用 Swift 来写,在命令式编程中,会写成这样:
func addTo(_ number: Int) -> Int {
var sum: Int = 0
for i in 1...number {
sum += i
}
return sum
}
addTo(10) // 55
addTo() 是有状态的,sum 为一个执行状态,在执行结束的时候程序达到了最终状态,目标达到,结束运行。
而在函数式编程中,代码会是这样:
func addUpTo(_ number: Int) -> Int {
if number >= 1 {
return number + addUpTo(number-1)
}
return number
}
addUpTo(10) // 55
函数式编程中不使用循环,想要循环只能使用递归。在 addUpTo() 中是不保存状态的,换个方式,可以说,它是使用了函数来保存状态的。
在函数式编程中,如果仅此而已,那么其表达能力是非常弱的,函数式编程有两个本质:
- 高阶函数:我们可以将函数作为另外一个函数的参数或者返回值,这样组合下来的函数即为高阶函数。
- 没有副作用:函数的功能是返回一个新值,没有其他行为,更不能修改外部变量的值,强于计算,弱与 I/O。
当然函数式编程不可能就真的不执行 I/O,但它通过一些手段来把I/O的影响限制到最小,比如通过Continuations, Monad等技术。
Moand
重点来了: 说了那么多函数式编程,就是为了引出 Monad,Moand 是将函数连接起来的方法,在函数式编程里是一个比较重要的概念。
Monad 提供了一个 >>=
方法,其发音是 bind。通过 bind 可以完成两个函数的绑定,完成一个组合计算。
因为对函数式和 Monad 的理解也不是非常深刻,在这里就不展开了,上面两个链接也只是从茫茫资料中挑选出来好理解的。毕竟是做程序,多敲代码才能更加理解。
在第一个知乎上面的回答中 Monad 的例子,写了一个 Swift 版本,方便理解吧:
enum type {
case ret
case excp
}
// M 是一个 Monad
struct M <T> {
init(value: T, t: type) {
self.value = value
self.t = t
}
var value: T
var t: type
// 一个辅助 debug 方法
func log() {
if t == .ret {
print(value)
} else {
print("Exception")
}
}
// 将一个普通值放在一个 M 中
static func unit (value: T) -> M {
return M.init(value: value, t: .ret)
}
// 一个不可用的 M
static func raise () -> M {
return M.init(value: 0 as! T, t: .excp)
}
// >>= 方法
func selfBind (value: M, morph: ((T) -> M)) -> M {
if value.t == .excp {
return M.raise()
} else {
return morph(value.value)
}
}
// Swift 优化版 >>= 方法
func bind (morph: ((T) -> M)) -> M {
return selfBind(value: self, morph: morph)
}
}
M.unit(value: 8).bind(morph: { (v1: Int) -> M<Int> in
return M.unit(value: 2).bind(morph: { (v2: Int) -> M<Int> in
if v2 == 0 {
return M.raise()
}
return M.unit(value: v1/v2)
})
}).log() // 4
M.unit(value: 8).bind { (v1: Int) -> M<Int> in
return M.unit(value: 0).bind(morph: { (v2: Int) -> M<Int> in
if v2 == 0 {
return M.raise()
}
return M.unit(value: v1/v2)
})
}.log() // Exception
响应式编程 (Reactive Programming)
响应式编程是 ReactiveCocoa 的主要思想,Monad 是实现这种思想的方式。
函数响应式编程(Functional Reactive Programming:FRP)是一种和事件流有关的编程方式,其角度类似EventSoucing,关注导致状态值改变的行为事件,一系列事件组成了事件流。
FRP是更加有效率地处理事件流,而无需显式去管理状态。
这里也不展开讨论了,下面是两个讲解:
响应式编程 (Reactive Programming) 的关键点在事件流(Stream):
-
可以用包括Click和Hover事件在内的任何东西创建Data stream,任何东西都可以是一个Stream:变量、用户输入、属性、Cache、数据结构等等。
-
在这个基础上,你还有令人惊艳的函数去combine、create、filter这些Stream。
-
Stream就是一个 按时间排序的Events(Ongoing events ordered in time)序列 ,它可以发送三种不同的Events:(某种类型的)Value、Error或者一个"Completed" Signal。
-
监听一个Stream也被称作是 订阅(Subscribing),而我们所定义的函数就是观察者(Observer),Stream则是被观察者(Observable),其实就是观察者模式(Observer Design Pattern)。
理解了以上两篇文章以后,对于 ReactiveCocoa 的理解就会简单很多了。
ReactiveCocoa 实现
在上一个章节中了解到,ReactiveCocoa 实现了 FRP,也了解了什么是 FRP。再温习一下,FRP 的关键点在于:
- Stream
- Subscribe
再次来看一下一个 Stream 的整个订阅过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ox8hKmk-1660639451687)(https://s-media-cache-ak0.pinimg.com/564x/5a/35/2b/5a352b2c8ea79df647dff9d0c00425aa.jpg)]
而对于 ReactiveCocoa 来说,其重点实现为:
- RACStream:对应响应式中的 Stream,它是一个 Monad。
- RACSubscriber:做订阅动作的订阅者。
- RACSignal:RACStream 在 ReactiveCocoa 中只是一个流的抽象,而 RACSignal 以及其类簇才是真正实现功能的地方。
- RACDisposable:当一个订阅者结束订阅的时候,需要将在订阅的时候创建的数据清除或者将创建的其他任务结束,约等于收尾工作。
- RACScheduler:封装了 GCD,用来控制任务在什么时候什么位置执行。在订阅的一个 Signal 的时候,RACScheduler 将创建 RACSignal 的时候传入的 block 放在了线程中执行。
看起来是比纯粹的 RP 订阅过程要复杂一些,其实重点还是 RACSignal
和 RACSubscriber
其他的东西都是围绕这这俩进行的,先看一下 Reactivecocoa 整个订阅过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fTAfPlPR-1660639451691)(https://s-media-cache-ak0.pinimg.com/564x/48/d5/0d/48d50d9c27594e72cd31822131725d92.jpg)]
解读一下(具体代码在后面):
- Create Signal with stream block:创建 RACSignal,需要传入一个 block (这个 block 会带一个 RACSubscriber 类型的参数,并返回一个 RACDisposable), 然后将这个 block 存入属性
didSubscribe
,在这个 block 中指定数据或者事件流的内容,并指定在某一个状态执行 block 自带的参数 subscriber 的sendNext
、sendError
、sendComplete
方法。注意在这一步,只是创建了 RACSignal 其他的东西都没有创建,block 中的代码并未执行,因此,如果没有订阅一个 Signal 那么这个 Signal 将没有任何的数据或事件流,也不存在订阅者; - Subscribe:订阅 Signal,调用 signal 的
subscribeNext...
方法,指定订阅者对应的nextBlock
、errorBlock
、completeBlock
,并且执行didSubscribe(subscriber)
这里的 subscriber 就是创建的订阅者; - sendNext:sendNext 操作是第二步中创建的 subscriber 调用的;
- sendError:sendError 操作是第二步中创建的 subscriber 调用的;
- sendComplete: sendComplete 操作是第二步中创建的 subscriber 调用的;
- dispose:在创建 RACSignal 的时候,会指定其 RACDisposable,并在第二步的时候将这个 disposable 存入 subscriber,在 subscriber 调用
sendError
、sendComplete
的时候调用 dispose 方法执行清理代码。
RACSignal
先来看看 RACSignal 的继承关系:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uYFNNrdc-1660639451693)(https://s-media-cache-ak0.pinimg.com/564x/41/9f/3b/419f3bc953aa42a3b20dd9423f786b7e.jpg)]
RACStream 是一个 Monad,代表了一个数据或事件流,它声明了 reutrn
和 bind
方法,实现里面直接返回了 nil,因为它是一个抽象类,我们在用的时候最多使用的是 RACSignal
和 RACSequence
(它也是 RACStream 的子类,可以先不去思考这个)。
/// RACStream.h
///
/// A block which accepts a value from a RACStream and returns a new instance
/// of the same stream class.
///
/// Setting `stop` to `YES` will cause the bind to terminate after the returned
/// value. Returning `nil` will result in immediate termination.
typedef RACStream * (^RACStreamBindBlock)(id value, BOOL *stop);
/// An abstract class representing any stream of values.
///
/// This class represents a monad, upon which many stream-based operations can
/// be built.
///
/// When subclassing RACStream, only the methods in the main @interface body need
/// to be overridden.
@interface RACStream : NSObject
/// Lifts `value` into the stream monad.
///
/// Returns a stream containing only the given value.
+ (instancetype)return:(id)value;
/// Lazily binds a block to the values in the receiver.
///
/// This should only be used if you need to terminate the bind early, or close
/// over some state. -flattenMap: is more appropriate for all other cases.
///
/// block - A block returning a RACStreamBindBlock. This block will be invoked
/// each time the bound stream is re-evaluated. This block must not be
/// nil or return nil.
///
/// Returns a new stream which represents the combined result of all lazy
/// applications of `block`.
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;
当然 RACSignal 也是一个 Monad,它的功能是通过一系列的类簇来实现的:
-
RACReturnSignal: 实现了
return
方法和subscribe
(订阅)方法,其订阅方法在订阅的时候会直接调用sendNext
和sendComplete
方法。 -
RACEmptySignal: 实现了
empty
方法(可以类比上面 Swift 代码中Struct M
中的raise()
方法)和subscribe
(订阅)方法,其订阅方法在订阅的时候会直接调用sendComplete
方法。 -
RACErrorSignal: 实现了
error
方法和subscribe
(订阅)方法,其订阅方法在订阅的时候会直接调用sendError
方法。在某些时候如果创建 Signal 出错的时候将会被创建,存储了错误信息。 -
RACDynamicSignal: 实现了
createSignal
、subscribe
方法,一个正确可用的 RACSignal 其实是这个类型。 -
RACSubject: 可以作为一个
subscriber
来订阅其他的信号,也可以作为一个信号被订阅。它实现了RACSubscriber
协议中的sendNext
、sendError
、sendComplete
方