Swift Combine 中的热与冷发布者

发布: (2025年12月24日 GMT+8 00:57)
6 min read
原文: Dev.to

Source: Dev.to

(请提供您希望翻译的具体文本内容,我将为您翻译成简体中文并保留原有的格式、Markdown 语法以及技术术语。)

Source:

什么是热发布者和冷发布者?

冷发布者

冷发布者 为每个订阅者创建一次新的执行。当你订阅时,工作从头开始。

let coldPublisher = Deferred {
    Future { promise in
        print("Making API call")
        // Simulate API call
        promise(.success(Int.random(in: 1...100)))
    }
}

coldPublisher.sink { print("Subscriber 1: \($0)") }
// Output: "Making API call", "Subscriber 1: 42"

coldPublisher.sink { print("Subscriber 2: \($0)") }
// Output: "Making API call", "Subscriber 2: 87"

每个订阅者都会触发一次独立的 API 调用。

常见的冷发布者

  • Just
  • Fail
  • Deferred
  • URLSession.dataTaskPublisher
  • Future(略带冷特性)

热发布者

热发布者 在所有订阅者之间共享一次执行。工作独立于订阅者进行,每个订阅者从它订阅的那一刻起接收事件。

let hotPublisher = PassthroughSubject<Int, Never>()

hotPublisher.sink { print("Subscriber 1: \($0)") }

hotPublisher.send(1)
// Output: "Subscriber 1: 1"

hotPublisher.sink { print("Subscriber 2: \($0)") }

hotPublisher.send(2)
// Output: "Subscriber 1: 2"
// Output: "Subscriber 2: 2"

两个订阅者都从同一次发射中收到相同的值。

常见的热发布者

  • PassthroughSubject
  • CurrentValueSubject
  • Timer.publish
  • NotificationCenter.publisher

为什么这很重要

1. 资源管理

冷发布者可能会浪费资源:

let apiCall = URLSession.shared.dataTaskPublisher(for: url)

apiCall.sink { print("UI: \($0)") }
apiCall.sink { print("Cache: \($0)") }

上面的代码会发起两个独立的网络请求。在很多情况下,你希望一个请求在多个订阅者之间共享。

2. 状态一致性

热发布者确保所有订阅者看到相同的数据:

let userState = CurrentValueSubject<User?, Never>(nil)

// All parts of your app observe the same user state
userState.sink { user in updateUI(user) }
userState.sink { user in syncToDatabase(user) }

你的应用的所有部分都观察相同的用户状态。

3. 时序问题

如果你期望共享执行,冷发布者可能导致竞争条件:

let timestamp = Just(Date())

timestamp.sink { print("Time 1: \($0)") }
Thread.sleep(forTimeInterval: 1)
timestamp.sink { print("Time 2: \($0)") }

每个订阅者都会得到不同的时间戳,因为 Just 会在每次订阅时创建一个新值。

何时使用哪种

使用冷发布者的情形

你希望每个订阅者都有独立的执行

func fetchUserData(id: String) -> AnyPublisher<User, Error> {
    URLSession.shared.dataTaskPublisher(for: makeURL(id))
        .map(\.data)
        .decode(type: User.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

// Each call makes its own request
fetchUserData(id: "123").sink {  }
fetchUserData(id: "456").sink {  }

你在创建可复用的发布者工厂

func createTimer(interval: TimeInterval) -> AnyPublisher<Date, Never> {
    Deferred {
        Timer.publish(every: interval, on: .main, in: .common)
            .autoconnect()
    }
    .eraseToAnyPublisher()
}

你希望惰性求值 —— 只有在真正有订阅者时才会执行工作。

使用热发布者的情形

你需要向多个观察者广播事件

class LocationManager {
    let locationPublisher = PassthroughSubject<CLLocation, Never>()

    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        locations.forEach { locationPublisher.send($0) }
    }
}

你在管理共享状态

class AppState {
    let isLoggedIn = CurrentValueSubject<Bool, Never>(false)
    let currentUser = CurrentValueSubject<User?, Never>(nil)
}

你需要共享昂贵的操作

let sharedAPICall = apiPublisher
    .share()
    .eraseToAnyPublisher()

// Multiple subscribers, single network request
sharedAPICall.sink { updateUI($0) }
sharedAPICall.sink { cacheData($0) }

转换冷热

冷 → 热:使用 .share()

let coldPublisher = URLSession.shared.dataTaskPublisher(for: url)
let hotPublisher = coldPublisher.share()

// Now multiple subscribers share one request
hotPublisher.sink { print("Subscriber 1: \($0)") }
hotPublisher.sink { print("Subscriber 2: \($0)") }

热 → 冷:使用 Deferred

let hotPublisher = PassthroughSubject<Int, Never>()

let coldPublisher = Deferred { hotPublisher }

// Each subscription creates a new relationship to the subject

将热转换为冷在实际使用中很少需要。

常见陷阱

陷阱 1:无意的多次执行

// Bad: Makes 3 separate API calls
let apiCall = URLSession.shared.dataTaskPublisher(for: url)
apiCall.sink { handleResponse($0) }
apiCall.sink { cacheResponse($0) }
apiCall.sink { logResponse($0) }

// Good: Makes 1 API call, shared among subscribers
let sharedCall = apiCall.share()
sharedCall.sink { handleResponse($0) }
sharedCall.sink { cacheResponse($0) }
sharedCall.sink { logResponse($0) }

陷阱 2:缺少订阅副作用

If you create a hot publisher but never subscribe, the work never starts:

let timer = Timer.publish(every: 1.0, on: .main, in: .common)
// No `.autoconnect()` or `sink` → timer never fires

Always ensure a hot publisher has at least one active subscriber (or call .autoconnect() for Timer.publish).

陷阱 3:使用 .share() 导致内存泄漏

let shared = expensivePublisher.share()

// If no subscribers exist, .share() can keep resources alive
// Consider using .share(replay: 0) or managing lifecycle explicitly

缺失的事件与热发布者

let subject = PassthroughSubject<String, Never>()

subject.send("Event 1") // Lost, no subscribers yet

subject.sink { print($0) } // Subscribes now

subject.send("Event 2") // Received

如果需要后来的订阅者接收值,请使用 CurrentValueSubject 或者重放操作符。

快速参考

冷发布者特性

  • 每个订阅者都有新的执行
  • 订阅时开始工作
  • 每个订阅者获得独立的结果
  • 适用于应重复的操作

热发布者特性

  • 在订阅者之间共享执行
  • 工作独立于订阅而发生
  • 所有订阅者接收相同的事件
  • 适用于广播和共享状态

结论

了解何时使用热发布者与冷发布者有助于避免错误、优化性能,并编写更可预测的响应式代码。

经验法则:

  • 如果多个订阅者应共享相同的工作或状态,请使用 发布者。
  • 如果每个订阅者应触发独立的工作,请使用 发布者。
Back to Blog

相关文章

阅读更多 »

Combine #2:发布者与订阅者

Publisher 一个发布者可以向一个或多个订阅者发送零个或多个值,并且只能发送一次结束事件,该事件可以是成功或错误。一旦发出…

SwiftUI 手势系统内部

markdown !Sebastien Latohttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%...

SwiftUI 视图差分与调和

SwiftUI 并不会“重新绘制屏幕”。它对视图树进行差异比较。如果你不了解 SwiftUI 如何决定哪些发生了变化、哪些保持不变,你会看到不必要的…