Combine:核心概念

8,612 阅读5分钟

在上一篇概览 中说过,Combine有三个核心概念:Publisher、Operator和Subscriber。PublisherSubscriber 分别代表事件的发布者和订阅者,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 指定的结果。

关于常用操作符,这篇文章 介绍的十分全面,可做参考。

参考

Combine: Asynchronous Programming with Swift

SwiftUI和Combine编程