第二章 Publisher 和 Subscriber (Part. 1)

1,586 阅读8分钟

Publiser

简介

Combine 的核心是发布者协议。该协议定义了一种类型的要求,以便能够随时间向一个或多个订阅者传输一系列值。换句话说,你可以将感兴趣的值,用发布者将其用事件的方式发布出去。

发布者可以发出零个或多个值,但只能发出一个完成事件,可以是正常的完成事件,也可以是错误。一旦发布者发出完成事件,它就完成了,不能再发出任何事件。

在实际应用中,我们用Publishers来启动我们Combine流程,也可以称为Combine链,试想下你如果想观察一个数据的变化,那么我们要把数据放到Combine流程中,然后我们可以用订阅的方式来异步的接收他的变化值。这些的关键起始点,就是要把数据,用Publishers发布出去。 比如我们可以用PassthroughSubject和CurrentValueSubject发布数据,同时发布错误; 可以用Just发布一个错误类型默认为Never的数据,等等。

所以,实际应用中使用Combine的核心思想之一,就是将数据当成Publishers的Output,开始Combine流程。

代码说明

我们在playground中观察如下代码:

// 1
var cancellable = Set<AnyCancellable>()
// 2
let myPublisher = PassthroughSubject<Int, Never>()
// 3
myPublisher
	.sink(receiveCompletion: { completion in
 		// 4
 		switch(completion) {
 		case .finished:
 			print("finished")
 		case .failure(let error):
 			print(error.localizedDescription)
 		}
 	}, receiveValue: { value in
 		// 5
 		print(value)
 	})
 	.store(in: &cancellable)
 
// 6
myPublisher.send(2)
// 7
myPublisher.send(completion: .finished)
// 8
myPublisher.send(3)

1、声明AnyCancellable的集合,用于管理Combine的内存 2、声明一个Publisher,我们用PassthroughSubject来返回一个Publisher,如果您没有使用过,也不要害怕,后面我们会详细说明这个Publisher,同时他也是我们会经常用到的一个生成Publisher的函数。 他生成了一个Output值为Int型,错误类型为Never,也就是永远不会出现错误。 3、开始Combine的订阅流程,用sink来实现订阅,也就是第一章提到的Subscriptions 4、处理订阅的状态,分为完成状态(.finished)和错误状态(.failure(error)) 5、处理数据接收,在这里我们将数据打印 6、开始发布数据,首先发布了一个整型数据(2) 7、发布结束状态,表示订阅完成 8、再次发布一个数据,发布了一个整型数据(3)

我们在myPublisher.send(3)点击运行,看到打印如下

2 
finished

表示我们收到了第一次发出的整型数据(2),同时收到了第二次发出的结束状态 ( .finished ),我们没有收到第三次发出的整型数据(3),这是因为在第二次接收到结束状态(.finsished)时,订阅就结束了,后续我们发送的任何数据都不会再进入Combine的流程中。

现在我们屏蔽掉

// myPublisher.send(completion: .finished)

然后在两次发布整型数据中间,加入一行

// 6
myPublisher.send(2)

// 7
//myPublisher.send(completion: .finished)
myPublisher.send(completion: .failure())

// 8
myPublisher.send(3)

我们尝试下不发送 (.finished),而是发送错误状态(.failure),可以看到,(.failure)需要一个参数,也就是具体错误。这里我们需要自定义个一个错误类型,要遵循Swift标准库的Error协议。

enum MyError: Swift.Error {
 case someError
}

然后将其填写到(.failure)中

myPublisher.send(completion: .failure(MyError.someError))

结果,编译器报错了,错误内容是

 Cannot convert value of type 'MyError' to expected argument type 'Never'

因为我们声明的Publishers是PassthroughSubject<Int, Never>,也就是错误类型是Never,那么我们就不能用它来发布错误。所以我们将其改为PassthroughSubject<Int, Error>

现在完整代码为:

// 1
var cancellable = Set<AnyCancellable>()
// 2
let myPublisher = PassthroughSubject<Int, Error>()

// 3
myPublisher
 	.sink(receiveCompletion: { completion in
 		// 4
 		switch(completion) {
 		case .finished:
 			print("finished")
 		case .failure(let error):
 			print(error.localizedDescription)
 		}
 	}, receiveValue: { value in
 		// 5
 		print(value)
 	})
 	.store(in: &cancellable)

// 6
myPublisher.send(2)

// 7
//myPublisher.send(completion: .finished)
myPublisher.send(completion: .failure(MyError.someError))

// 8
myPublisher.send(3)

现在运行,得到打印

> 2
>The operation couldn’t be completed. (__lldb_expr_225.MyError error 0.)

可见,当有错误发生时,订阅流程也被终止了。

Subscriber

订阅者是一种协议,它定义了能够从发布者接收输入的类型的要求。

需要注意的是,如果声明了发布者,但如果没有任何订阅者订阅它的话,发布者无法发出任何数据。

常用的订阅方式在第一章提到过,有两种,分别是:

  • sink

    sink 操作符实际上提供了两个闭包:一个处理接收完成事件(成功或失败),一个处理接收值。
    

    写法如下

    .sink(
      receiveCompletion: {
        print("Received completion", $0)
      },
      receiveValue: {
        print("Received value", $0)
    })”
    
  • assign

    assign 运算符使您能够将接收到的值分配给对象
    
class SomeObject {
    var value: String = "" {
      didSet {
        print(value)
      }
    }
  }
  
  let object = SomeObject()

  let publisher = ["Hello", "world!"].publisher
    .assign(to: \.value, on: object)
 

Cancellable

当订阅者完成其工作并且不再希望从发布者接收值时,取消订阅以释放资源并停止任何相应活动的发生是一个好主意,例如网络调用。 订阅返回 AnyCancellable 的一个实例作为“取消令牌”,这使得您可以在完成订阅后取消订阅。 AnyCancellable 符合 Cancellable 协议,它需要使用 cancel() 方法来实现这一目的.

在前面 Subscriber 示例的底部,您添加了代码 subscription.cancel()。您可以对订阅调用 cancel(),因为订阅协议继承自 Cancelable。 如果您没有在订阅上显式调用 cancel(),它将一直持续到发布者完成,或者直到正常的内存管理导致存储的订阅取消初始化。届时,它会为您取消订阅

AnyPublisher

AnyPublisher 是一个类型擦除结构,符合Publisher协议。类型擦除允许您隐藏您可能不想向订阅者或下游发布者公开的有关发布者的详细信息。

在Combine流程中,我们经常使用.eraseToAnyPublisher()来将类型擦出,最后得到一个AnyPublisher类型的发布者。

AnyPublisher多用于函数或计算变量的返回值.

再看下Combine的流程

Combine 的简单流程图

img881.jpg

从图中我们可以看出:

  1. 订阅者(Subscriber)对发布者(Publisher)提出订阅
  2. 发布者(Publisher)同意订阅
  3. 订阅者(Subscriber)请求获取数据
  4. 发布者(Publisher)发布数据
  5. 发布者(Publisher)发布完成标志

Combine 的三要素

发布者和订阅者是相互连接的,它们构成了 Combine 的核心。当您将订阅者连接到发布者时,两种类型都必须匹配:输出到输入和失败到失败。将其可视化的一种方法是对两种类型进行一系列并行操作,其中两种类型都需要匹配才能将组件插入在一起。

input_output.png

操作符——一个既像订阅者又像发布者的对象。运算符是同时采用订阅者协议和发布者协议的类。它们支持订阅发布者,并将结果发送给任何订阅者。

您可以一起创建这些链,用于处理、反应和转换发布者提供的和订阅者请求的数据。

我将这些组合序列称为管道(Pipelines)。

pipeline.png

用代码来观察Combine的流程

我们再回顾一下第一章中的例子代码,并在sink之前加一行 .print("_Debug_")

// 声明一个CurrentValueSubject类型的Publisher
let curPublisher = CurrentValueSubject<String, Never>("No.1")

// 声明一个AnyCancellable类型的对象
let cancellable: AnyCancellable = curPublisher
 	.print("_Debug_")		// 新增加
 	.sink { completion in
 		switch completion {
 		case .finished:
 			print("finished")
 		case .failure(let error):
 			print("Got a error: \(error)")
 		}
 	} receiveValue: { str in
 		print(str)
 }

print操作符是Combine的一种调试方式,参数是可以在打印信息中显示的调试信息头,上面代码运行后结果为

_Debug_: receive subscription: (CurrentValueSubject)
_Debug_: request unlimited
_Debug_: receive value: (No.1)
No.1

和简单流程图相比,我们可以看出,加入sink,相当于进行了订阅,也就是流程图的第一步和第二步。

receive subscription: (CurrentValueSubject)

订阅后,开始请求数据

request unlimited

同时,发布者发布数据,同时订阅者接收数据

receive value: (No.1)

Publisher 协议

详细了解Publisher,我们可以先看下Publisher的定义:

public protocol Publisher {
  // 1
  associatedtype Output

  // 2
  associatedtype Failure : Error

  // 4
func receive<S>(subscriber: S)
    where S: Subscriber,
    Self.Failure == S.Failure,
    Self.Output == S.Input
}

extension Publisher {
  // 3
public func subscribe<S>(_ subscriber: S)
    where S : Subscriber,
    Self.Failure == S.Failure,
    Self.Output == S.Input
}
  1. Publisher可以发布的数据。
  2. Publisher可能产生的错误类型,如果保证发布者不会产生错误,则使用Never。 3&4. 将特定的Subscriber挂接到Publisher,一般用于自定义的Subscriber

其中subscribe和receive的区别为:

  • subscribe 设置您希望在其上“管理”当前订阅的调度程序。此运算符设置调度程序以用于创建订阅、取消和请求输入。
    换句话说 subscribe设置调度器订阅上游。
    订阅给定调度程序的一个副作用是 subscribe也会更改其下游的调度程序

  • receive用于更改传递下游输出。 换句话说,receive设置下游接收输出的调度程序。
    您可以使用 receive类似于 subscribe。您可以使用多个 receive运算符,这将始终更改下游调度程序

Subscriber协议

public protocol Subscriber: CustomCombineIdentifierConvertible {
	  // 1
	  associatedtype Input

	 // 2
	  associatedtype Failure: Error

	  // 3
	 func receive(subscription: Subscription)

	  // 4
	  func receive(_ input: Self.Input) -> Subscribers.Demand

	  // 5
	  func receive(completion: Subscribers.Completion<Self.Failure>)
}
  1. 订阅者接收的数据
  2. 订户可能收到的错误类型,或者 Never 。
  3. 发布者对订阅者调用 receive(subscription:) 来给它订阅。
  4. 发布者在订阅者上调用 receive(_:) 向它发送一个它刚刚发布的新值。
  5. 发布者对订阅者调用 receive(completion:) 来告诉它它已经完成了生成值,无论是正常的还是由于错误。

订阅协议

发布者和订阅者之间的联系是订阅。这是订阅协议

public protocol Subscription: Cancellable, CustomCombineIdentifierConvertible { func request(_ demand: Subscribers.Demand) }

订户调用 request(_:) 表示它愿意接收更多值,最多可达最大数量或无限制

在 Subscriber 中,请注意 receive(:) 返回一个 Demand。即使 subscription.request(:) 设置了订阅者愿意接收的初始最大值数,您也可以在每次收到新值时调整该最大值