首页 > 解决方案 > 如何将 DispatchQueue debounce 转换为 Swift 并发任务?

问题描述

我有一个使用DispatchQueue. 它接受一个闭包并在达到时间阈值之前执行它。它可以这样使用:

let limiter = Debouncer(limit: 5)
var value = ""

func sendToServer() {
    limiter.execute {
        print("\(Date.now.timeIntervalSince1970): Fire! \(value)")
    }
}

value.append("h")
sendToServer() // Waits until 5 seconds
value.append("e")
sendToServer() // Waits until 5 seconds
value.append("l")
sendToServer() // Waits until 5 seconds
value.append("l")
sendToServer() // Waits until 5 seconds
value.append("o")
sendToServer() // Waits until 5 seconds
print("\(Date.now.timeIntervalSince1970): Last operation called")

// 1635691696.482115: Last operation called
// 1635691701.859087: Fire! hello

请注意,它不是Fire!多次调用,而是在最后一次使用最后一个任务的值后 5 秒。该Debouncer实例被配置为将队列中的最后一个任务保持 5 秒,无论它被调用多少次。闭包被传递到execute(block:)方法中:

final class Debouncer {
    private let limit: TimeInterval
    private let queue: DispatchQueue
    private var workItem: DispatchWorkItem?
    private let syncQueue = DispatchQueue(label: "Debouncer", attributes: [])
   
    init(limit: TimeInterval, queue: DispatchQueue = .main) {
        self.limit = limit
        self.queue = queue
    }
    
    @objc func execute(block: @escaping () -> Void) {
        syncQueue.async { [weak self] in
            if let workItem = self?.workItem {
                workItem.cancel()
                self?.workItem = nil
            }
            
            guard let queue = self?.queue, let limit = self?.limit else { return }
            
            let workItem = DispatchWorkItem(block: block)
            queue.asyncAfter(deadline: .now() + limit, execute: workItem)
            
            self?.workItem = workItem
        }
    }
}

如何将其转换为并发操作,以便可以如下调用:

let limit = Debouncer(limit: 5)

func sendToServer() {
    await limiter.waitUntilFinished
    print("\(Date.now.timeIntervalSince1970): Fire! \(value)")
}

sendToServer()
sendToServer()
sendToServer()

但是,这不会取消任务,而是暂停它们直到下一个被调用。相反,它应该取消前一个任务并保持当前任务直到去抖动时间。这可以用Swift Concurrency完成还是有更好的方法来做到这一点?

标签: swiftgrand-central-dispatchdebounceswift-concurrency

解决方案


任务有能力使用isCancelledor checkCancellation,但是为了去抖动例程,你想等待一段时间,你可能只使用 的 throwing rendition Task.sleep(nanoseconds:),其文档说:

如果任务在时间结束前被取消,这个函数会抛出CancellationError.

因此,这有效地消除了 2 秒的抖动。

var task: Task<(), Never>?

func debounced(_ string: String) {
    task?.cancel()

    task = Task {
        do {
            try await Task.sleep(nanoseconds: 2_000_000_000)
            logger.log("result \(string)")
        } catch {
            logger.log("canceled \(string)")
        }
    }
}

(为什么苹果恢复到纳秒是我无法理解的。)

注意,non-throwing 的渲染sleep(nanoseconds:)不会检测到取消,所以你必须使用这个 throwing 的渲染。


推荐阅读