在上一篇概览 中说过,Combine有三个核心概念:Publisher、Operator和Subscriber。Publisher 和 Subscriber 分别代表事件的发布者和订阅者,Operator兼具两者的特性,它同时遵循Subscriber和 Publisher协议,用来对上游数据进行操作。
只要理解了这三个核心概念,你就可以很好的使用Combine,所以从这个角度来说,我们可以将Combine简单的理解为下面的形式:
Combine = Publishers + Operators + Subscribers
Publisher
定义:
public protocol Publisher<Output, Failure> {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
复制代码
publisher被订阅后,会根据subscriber的请求提供数据,一个没有任何subscriber的publisher不会发出任何数据。
Publisher可以发布三种事件:
- Output:事件流中出现的新值
- Finished: 事件流中所有元素发布结束,事件流完成使命并终结
- Failure: 事件流中发生了错误,事件流到此终结
Finished和Failure事件被定义在Subscribers.Completion中
extension Subscribers {
@frozen public enum Completion<Failure> where Failure : Error {
case finished
case failure(Failure)
}
}
复制代码
三种事件都不是必须的。Publisher 可能会发出一个或多个 output 值,也可能不发出任何值;它可能永远不会停止,也可能会通过发出failure 或 finished 事件来表示终结。
最终将会终止的事件流被称为有限事件流,而不会发出 failure 或者 finished 的事件流则被称为无限事件流。比如,一次网络请求就是一个有限事件流,而某个按钮的点击事件流就是无限事件流。
Subject
Subject也是一个Publisher
public protocol Subject : AnyObject, Publisher {
func send(_ value: Self.Output)
func send(completion: Subscribers.Completion<Self.Failure>)
}
复制代码
Subject 暴露了两个 send 方法,外部调用者可以通过这两个方法来主动地发布 output 值、failure 事件或 finished 事件。Subject可以将传统的指令式编程中的异步事件和信号转换到响应式的世界中去。
Combine内置了两种Subject类型:
-
PassthroughSubject
简单地将 send 接收到的事件转发给下游的其他 Publisher 或 Subscriber,不会持有最新的output;如果在订阅前执行send操作,是无效的。
let publisher1 = PassthroughSubject<Int, Never>()
print("开始订阅")
publisher1.sink(
receiveCompletion: { complete in
print(complete)
},
receiveValue: { value in
print(value)
})
publisher1.send(1)
publisher1.send(2)
publisher1.send(completion: .finished)
// 输出:
// 开始订阅
// 1
// 2
// finished
复制代码
调整一下 sink 订阅的时机,将它延后到 publisher.send(1)
之后,那么订阅者将会从 2 的事件开始进行响应:
let publisher2 = PassthroughSubject<Int, Never>()
publisher2.send(1)
print("开始订阅")
publisher2.sink(
receiveCompletion: { complete in
print(complete)
},
receiveValue: { value in
print(value)
})
publisher2.send(2)
publisher2.send(completion: .finished)
// 输出:
// 开始订阅
// 2
// finished
复制代码
-
CurrentValueSubject
会包装和持有一个值,并在设置该值时发送事件并保留新的值。在订阅发生的瞬间,会把当前保存的值发送给订阅者;接下来对值的每次设置都将触发订阅响应。
let publisher3 = CurrentValueSubject<Int, Never>(0)
print("开始订阅")
publisher3.sink(
receiveCompletion: { complete in
print(complete)
},
receiveValue: { value in
print(value)
})
publisher3.value = 1
publisher3.value = 2
publisher3.send(completion: .finished)
// 输出:
// 开始订阅
// 0
// 1
// 2
// finished
复制代码
Subscriber
定义:
public protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible {
associatedtype Input
associatedtype Failure : Error
func receive(subscription: Subscription)
func receive(_ input: Self.Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Self.Failure>)
}
复制代码
想要订阅某个 Publisher,Subscriber 中的这两个类型必须与 Publisher 的 Output 和 Failure 一致。
Combine 中也定义了几个比较常见的 Subscriber,可以供我们直接使用。
sink
sink的完整函数签名为
func sink(receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
复制代码
receiveCompletion
用来接收 failure 或者 finished 事件,receiveValue
用来接收 output 值。
let just = Just("Hello word!")
_ = just.sink(receiveCompletion: {
print("Received completion", $0)
}, receiveValue: {
print("Received value", $0)
})
复制代码
如果说Subject提供了一条从指令式异步编程通向响应式世界的道路的话,那么sink就补全了另外一侧。sink可以作为响应式代码和基于闭包的指令式代码之间的桥梁,让你可以通过它从响应式的世界中回到指令式的世界。因为receiveValue
闭包会将值带给你,想要对它做什么就随你愿意了。
assign
func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root,Self.Output>, on object:Root) -> AnyCancellable
复制代码
assign 接受一个 class 对象以及对象类型上的某个键路径 (key path)。每当 output 事件到来时,其中包含的值就将被设置到对应的属性上去。
定义一个MyObject类
class MyObject {
var value: String = "123" {
didSet {
print(value)
}
}
}
复制代码
使用 assign(to:on:)
修改MyObject实例对象属性的值
let obj = MyObject()
let _ = ["456"].publisher.assign(to: \.value, on: obj)
复制代码
assign 还有一个变体, assign(to:)
可将 Publisher 发出的值用于 @Published
属性包装器包装过的属性
class MyObject {
@Published var value = 0
}
let objc = MyObject()
objc.$value.sink {
print($0)
}
(0 ..< 5).publisher.assign(to: &objc.$value)
复制代码
value 属性用 @Published
包装,除了可作为常规属性访问之外,它还为属性创建了一个 Publisher。使用 @Published
属性上的 $
前缀来访问其底层 Publisher,订阅该 Publisher,并打印出收到的每个值。最后,我们创建一个 0..<5 的 Int Publisher 并将它发出的每个值 assign 给 object 的 value Publisher。 使用 &
来表示对属性的 inout 引用,这里的 inout 来源于函数签名:
func assign(to published: inout Published<Self.Output>.Publisher)
复制代码
这里有一个值得注意的地方,如果使用 assign(to: .value, on: self)
并存储生成的 AnyCancellable,可能会引起引用循环:MyObject 类实例持有生成的 AnyCancellable,而生成的 AnyCancellable 同样保持对 MyObject 类实例的引用。因此,推荐使用 assign(to:)
来替代 assign(to:on:)
,以避免此问题的发生,因为assign(to:)
::不返回 AnyCancellable,在内部完成了生命周期的管理,在 @Published
属性释放时会取消订阅。::
Operator
关于Operator的介绍,在概览中已经做了相对详细的介绍。
Operator 可以作为上游 Publisher 的输入,同时它们也可以成为新的 Publisher,输出处理过的数据给下游。我们可以把不同的操作符组合起来形成一个处理链:当链条最上端的 Publisher 发布事件或数据时,链条内的 Operator 会对这些数据和事件一步一步地进行处理,最终达到 subscriber 指定的结果。
关于常用操作符,这篇文章 介绍的十分全面,可做参考。