Swift Combine 中的热与冷发布者
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 调用。
常见的冷发布者
JustFailDeferredURLSession.dataTaskPublisherFuture(略带冷特性)
热发布者
热发布者 在所有订阅者之间共享一次执行。工作独立于订阅者进行,每个订阅者从它订阅的那一刻起接收事件。
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"
两个订阅者都从同一次发射中收到相同的值。
常见的热发布者
PassthroughSubjectCurrentValueSubjectTimer.publishNotificationCenter.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 或者重放操作符。
快速参考
冷发布者特性
- 每个订阅者都有新的执行
- 订阅时开始工作
- 每个订阅者获得独立的结果
- 适用于应重复的操作
热发布者特性
- 在订阅者之间共享执行
- 工作独立于订阅而发生
- 所有订阅者接收相同的事件
- 适用于广播和共享状态
结论
了解何时使用热发布者与冷发布者有助于避免错误、优化性能,并编写更可预测的响应式代码。
经验法则:
- 如果多个订阅者应共享相同的工作或状态,请使用 热 发布者。
- 如果每个订阅者应触发独立的工作,请使用 冷 发布者。