Combine 框架,從0到1 —— 2.通過 ConnectablePublisher 控制何時釋出
阿新 • • 發佈:2020-08-29
本文首發於 [Ficow Shen's Blog](https://ficowshen.com),原文地址: [Combine 框架,從0到1 —— 2.通過 ConnectablePublisher 控制何時釋出](https://blog.ficowshen.com/page/post/14)。
## 內容概覽
- 前言
- 使用 makeConnectable() 和 connect() 手動控制釋出
- 使用 autoconnect() 操作符進行自動連線
- 總結
## 前言
使用 `Connectable Publisher`, 你可以決定釋出者何時開始傳送訂閱元素給訂閱者。那麼,為什麼我們需要這麼做?
使用 `sink(receiveValue:)` 可以立刻開始接收訂閱元素,但是這可能不是你想要的結果。當多個訂閱者訂閱了同一個釋出者時,有可能會出現其中一個訂閱者收到訂閱內容,而另外一個訂閱者收不到的情況。
比如,當你發起一個網路請求,併為這個請求建立了一個釋出者以及連線了這個釋出者的訂閱者。
![圖片alt](https://ficowblog.oss-cn-shenzhen.aliyuncs.com/uploads/1597552131518.png)
然後,這個訂閱者的訂閱操作觸發了實際的網路請求。在某個時間點,你將第二個訂閱者連線到了這個釋出者。如果在連線第二個訂閱者之前,網路請求已經完成,那麼第二個訂閱者將只會收到完成事件,收不到網路請求的響應結果。這時候,這個結果將不是你所期望。
在使用 `Combine` 的過程中,我們往往需要面對這些問題。現在就來弄清楚如何處理這一類問題吧~
## 使用 makeConnectable() 和 connect() 控制釋出
`ConnectablePublisher` 是一個協議型別,它可以在你準備好之前阻止釋出者釋出元素。
``` swift
/// 可連線的釋出者,它提供了顯式的連線、取消訂閱的方式
///
/// 使用 `makeConnectable()` 來從任何一個失敗型別是 `Never` 的釋出者建立一個 `ConnectablePublisher`
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol ConnectablePublisher : Publisher {
/// 連線到釋出者並返回一個用於取消釋出的 `Cancellable` 例項
///
/// - 返回值: 一個用於取消釋出的 `Cancellable` 例項
func connect() -> Cancellable
}
```
在你顯式地呼叫 `connect()` 方法之前,一個 `ConnectablePublisher` 不會發送任何元素。
現在,就讓我們用 `ConnectablePublisher` 來解決上面提到的網路請求示例中的問題吧!
![ConnectablePublisher](https://ficowblog.oss-cn-shenzhen.aliyuncs.com/uploads/1597552861187.png)
在兩個訂閱者都連線到釋出者之後,呼叫 `connect()`,然後網路請求才被觸發。這樣就可以避免競爭(race condition),保證兩個訂閱者都收到資料。
為了在你的 Combine 程式碼中使用 `ConnectablePublisher`,你可以使用 `makeConnectable()` 操作符將當前的釋出者包裝到一個 `Publishers.MakeConnectable` 結構體例項中。
如下方的程式碼所示:
``` swift
class ConnectablePublisherDemo {
private var cancellable1: AnyCancellable?
private var cancellable2: AnyCancellable?
private var connection: Cancellable?
func run() {
let url = URL(string: "https://ficow.cn")!
let connectable = URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.catch() { _ in Just(Data()) }
.share()
.makeConnectable() // 阻止釋出者釋出內容
cancellable1 = connectable
.sink(receiveCompletion: { print("Received completion 1: \($0).") },
receiveValue: { print("Received data 1: \($0.count) bytes.") })
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.cancellable2 = connectable.sink(receiveCompletion: { log("Received completion 2: \($0).") },
receiveValue: { log("Received data 2: \($0.count) bytes.") })
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// 顯式地啟動釋出。返回值需要被強引用,可用於取消釋出(主動呼叫cancel方法或返回值被析構)
self.connection = connectable.connect()
}
}
}
```
請注意,在 `makeConnectable()` 操作符前面有一個 `share()` 操作符!請問,這個操作符有什麼作用呢?
## 使用 autoconnect() 操作符進行自動連線
某些 `Combine` 釋出者已經實現了 `ConnectablePublisher` 協議,如:`Publishers.Multicast` 和 `Timer.TimerPublisher`。使用這些釋出者時,如果你不需要配置釋出者或者不需要連線多個訂閱者,你就需要顯式地呼叫 `connect()` 方法。
對於這種情況,`ConnectablePublisher` 提供了 `autoconnect()` 操作符。當一個訂閱者通過 `subscribe(_:)` 方法連線到釋出者時,`connect()` 方法會被馬上呼叫。
``` swift
let cancellable = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink() { date in
print ("Date now: \(date)")
}
```
上面的程式碼示例中使用了 `autoconnect()`,所以訂閱者可以馬上接收到定時器傳送的元素。如果沒有 `autoconnect()`,我們就需要在某個時刻手動地呼叫 `connect()` 方法。
## 總結
`Combine` 為我們提供了很強大的非同步程式設計功能,不過這也是有代價的,我們需要深知使用 `Combine` 過程中可能會遭遇的問題。如果不瞭解這些`“坑”`就開始上路,犯錯的概率會非常高,犯錯的成本也會非常高。
**本文內容來源:** [Controlling Publishing with Connectable Publishers](https://developer.apple.com/documentation/combine/controlling-publishing-with-connectable-publishers),轉載請[註明出處](https://blog.ficowshen.com)。