onReceive如何知道不再对发布者重新订阅?

编程语言 2026-07-12

如果我有一个类似这样的视图

struct SomeView: View {
    @State var counter = 0
    static let subject = CurrentValueSubject<String, Never>(
        "hello world"
    ).handleEvents(receiveSubscription:  { newSub in
        print("newSub: \(newSub)")
    })
    var body: some View {
        VStack {
            Text("\(counter)")
            Button {
                counter += 1
            } label: {
                Text("increment counter")
            }

        }.onReceive(Self.subject) { newValue in
            print(newValue)
        }
    }
}

每当我增加计数器时,似乎不会导致对发布者进行重新订阅,这让我感到惊讶,因为该发布者没有实现Equatable协议。那么SwiftUI在重新计算body以知道不需要重新订阅时,怎么能知道发布者没有改变呢?(据我所知,SwiftUI会在更新视图之前,用更新后的值来比较视图的值以判断是否有变化,但这只有在视图的值实现了Equatable时才会起作用,对吧?)

对于onReceive的期望行为,据我理解,是因为发布者不能被比较,SwiftUI无法判断这是一个新的还是同一个发布者,因此会再次调用onReceive。

对我来说,onReceive是 SwiftUI中最怪异的部分,我还没有找到一段WWDC视频解释发布者如何与SwiftUI生命周期配合,以防止重复的取消订阅/重新订阅。

解决方案

发布者不会因为同样的原因被多次订阅,比如 .onAppear { ... }.task { ... } 中的代码不会多次运行。

视图是有身份标识的(参见 Demystifying SwiftUI)。只要视图的身份保持不变,SwiftUI就知道它仍然是同一个视图,不会再次执行传给 onAppear/task 的闭包。同样地,SwiftUI也不会再次订阅传给 onReceive 的发布者。

你可以通过仅使用 onAppearonDisappear,编写一个类似于 onReceive 的视图修饰符。

struct OnReceiveModifier<P: Publisher>: ViewModifier where P.Failure == Never {
    let publisher: P
    let perform: (P.Output) -> Void
    @State private var cancellable: (any Cancellable)?

    func body(content: Content) -> some View {
        content
            .onAppear {
                cancellable = publisher.sink(receiveValue: perform)
            }
            .onDisappear {
                cancellable?.cancel()
            }
    }
}
.modifier(OnReceiveModifier(publisher: Self.subject) {
    print($0)
})

这只是一个用来给你一个粗略心理模型的示意性例子。当然,实际的 onReceive 并不是这样实现的。首先,onReceive 会在下一次视图更新时重新订阅该发布者,如果现有订阅者在收到完成信号时。

SwiftUI.swiftinterface显示,onReceive 返回一个名为 SubscriptionView 的视图,它把 Never 作为自己的 Body。这意味着它是通过内部的AttributeGraph框架实现的,实现诸如 _makeView_makeViewList 等方法,而不是 body。它很可能使用自定义的 Subscriber 实现,而不是 sink

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章