You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Combine的sink内部取消订阅,避免回调重复触发

问题原因

你遇到的回调触发两次的问题核心来自CurrentValueSubject的特性和订阅执行顺序:

  • CurrentValueSubject存在默认初始值,当有新订阅产生时,会同步把当前最新值发送给订阅者,这个发送动作发生在.store(in:)方法执行之前
  • 第一次sink闭包被触发时,订阅还未被存入cancellableSet,执行到.doingSomething分支调用logUserOut方法,同步发送.loggedOut状态,触发第二次sink回调
  • 两次回调都执行完成后,订阅才会被存入cancellableSet,完全不符合你的预期。
解决方案

方案一:队列异步调度(最简洁)

在sink前添加receive(on:)操作符,让所有回调异步派发,保证订阅先存入集合再执行逻辑:

import Combine

enum State {
    case loggedOut
    case doingSomething
}

let aState = CurrentValueSubject<State, Never>(.doingSomething)
private var cancellableSet: Set<AnyCancellable> = []

func logUserOut() {
    cancellableSet.removeAll()
    aState.send(.loggedOut)
}

func doSomethingElse() { }

aState
    // 新增这行,所有回调派发到主队列异步执行,也可以替换为你自己的业务队列
    .receive(on: DispatchQueue.main)
    .sink { newState in
        print("numberOfSubscriptions is: \(cancellableSet.count)")
        switch newState {
        case .loggedOut:
            doSomethingElse()
        case .doingSomething:
            logUserOut()
        }
    }
    .store(in: &cancellableSet)

原理

添加receive(on:)后,事件回调会被放到队列的待执行列表里,当前线程会先走完订阅存储的逻辑,再执行回调,此时cancellableSet里已经存在对应的订阅,完全符合你的要求,且不需要用到take/drop操作符。

方案二:标记位控制(无队列依赖)

如果不想依赖队列调度,可以用标记位控制逻辑执行时机:

import Combine

enum State {
    case loggedOut
    case doingSomething
}

let aState = CurrentValueSubject<State, Never>(.doingSomething)
private var cancellableSet: Set<AnyCancellable> = []
// 新增订阅就绪标记位
private var isSubscriptionReady = false

func logUserOut() {
    cancellableSet.removeAll()
    aState.send(.loggedOut)
}

func doSomethingElse() { }

// 把状态处理逻辑抽为单独方法
func handleState(_ newState: State) {
    print("numberOfSubscriptions is: \(cancellableSet.count)")
    switch newState {
    case .loggedOut:
        doSomethingElse()
    case .doingSomething:
        logUserOut()
    }
}

aState
    .sink { newState in
        // 仅当订阅已经存入集合后才执行业务逻辑
        guard isSubscriptionReady else { return }
        handleState(newState)
    }
    .store(in: &cancellableSet)

// 订阅存入完成后再标记为就绪
isSubscriptionReady = true
// 手动处理当前最新的状态值,补上被跳过的初始值回调
handleState(aState.value)

内容的提问来源于stack exchange,提问作者Dr. Mr. Uncle

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 06:15:01