简体   繁体   English

如何将 DispatchQueue 去抖动转换为 Swift 并发任务?

[英]How to convert DispatchQueue debounce to Swift Concurrency task?

I have an existing debouncer utility using DispatchQueue .我有一个使用DispatchQueue的现有 debouncer 实用程序。 It accepts a closure and executes it before the time threshold is met.它接受一个闭包并在达到时间阈值之前执行它。 It can be used like this:它可以像这样使用:

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

Notice it is not calling Fire!注意它不是在调用Fire! multiple times, but just 5 seconds after the last time with the value from the last task.多次,但仅在上次使用上次任务的值后 5 秒。 The Debouncer instance is configured to hold the last task in queue for 5 seconds no matter how many times it is called. Debouncer实例配置为无论调用多少次,都会将队列中的最后一个任务保留 5 秒。 The closure is passed into the execute(block:) method:闭包被传递到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
        }
    }
}

How can I convert this into a concurrent operation so it can be called like below:如何将其转换为并发操作,以便可以像下面这样调用它:

let limit = Debouncer(limit: 5)

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

sendToServer()
sendToServer()
sendToServer()

However, this wouldn't debounce the tasks but suspend them until the next one gets called.但是,这不会使任务去抖动,而是将它们挂起,直到下一个被调用。 Instead it should cancel the previous task and hold the current task until the debounce time.相反,它应该取消前一个任务并保持当前任务直到去抖动时间。 Can this be done with Swift Concurrency or is there a better approach to do this?这可以用Swift Concurrency来完成还是有更好的方法来做到这一点?

Tasks have the ability to use isCancelled or checkCancellation , but for the sake of a debounce routine, where you want to wait for a period of time, you might just use the throwing rendition of Task.sleep(nanoseconds:) , whose documentation says:任务有能力使用isCancelledcheckCancellation ,但为了去抖动例程,你想等待一段时间,你可能只使用Task.sleep(nanoseconds:)的抛出再现,其文档说:

If the task is canceled before the time ends, this function throws CancellationError .如果任务在时间结束前被取消,该函数将抛出CancellationError

Thus, this effectively debounces for 2 seconds.因此,这有效地消除了 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)")
        }
    }
}

(Why Apple reverted to nanoseconds is beyond me.) (为什么 Apple 恢复到纳秒级是我无法理解的。)

Note, the non-throwing rendition of sleep(nanoseconds:) does not detect cancelation, so you have to use this throwing rendition.请注意, sleep(nanoseconds:)的非抛出再现不会检测到取消,因此您必须使用此抛出再现。

Based on @Rob's great answer, here's a sample using an actor and Task :基于@Rob 的精彩回答,这里有一个使用actorTask的示例:

actor Limiter {
    enum Policy {
        case throttle
        case debounce
    }

    private let policy: Policy
    private let duration: TimeInterval
    private var task: Task<Void, Never>?

    init(policy: Policy, duration: TimeInterval) {
        self.policy = policy
        self.duration = duration
    }

    nonisolated func callAsFunction(task: @escaping () async -> Void) {
        Task {
            switch policy {
            case .throttle:
                await throttle(task: task)
            case .debounce:
                await debounce(task: task)
            }
        }
    }

    private func throttle(task: @escaping () async -> Void) {
        guard self.task?.isCancelled ?? true else { return }

        Task {
            await task()
        }

        self.task = Task {
            try? await sleep()
            self.task?.cancel()
            self.task = nil
        }
    }

    private func debounce(task: @escaping () async -> Void) {
        self.task?.cancel()

        self.task = Task {
            do {
                try await sleep()
                guard !Task.isCancelled else { return }
                await task()
            } catch {
                return
            }
        }
    }

    private func sleep() async throws {
        try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
    }
}

The tests are inconsistent in passing so I think my assumption on the order of the tasks firing is incorrect, but the sample is a good start I think:测试在通过时不一致,所以我认为我对任务触发顺序的假设是不正确的,但我认为示例是一个好的开始:

final class LimiterTests: XCTestCase {
    func testThrottler() async throws {
        // Given
        let promise = expectation(description: "Ensure first task fired")
        let throttler = Limiter(policy: .throttle, duration: 1)
        var value = ""

        var fulfillmentCount = 0
        promise.expectedFulfillmentCount = 2

        func sendToServer(_ input: String) {
            throttler {
                value += input

                // Then
                switch fulfillmentCount {
                case 0:
                    XCTAssertEqual(value, "h")
                case 1:
                    XCTAssertEqual(value, "hwor")
                default:
                    XCTFail()
                }

                promise.fulfill()
                fulfillmentCount += 1
            }
        }

        // When
        sendToServer("h")
        sendToServer("e")
        sendToServer("l")
        sendToServer("l")
        sendToServer("o")

        await sleep(2)

        sendToServer("wor")
        sendToServer("ld")

        wait(for: [promise], timeout: 10)
    }

    func testDebouncer() async throws {
        // Given
        let promise = expectation(description: "Ensure last task fired")
        let limiter = Limiter(policy: .debounce, duration: 1)
        var value = ""

        var fulfillmentCount = 0
        promise.expectedFulfillmentCount = 2

        func sendToServer(_ input: String) {
            limiter {
                value += input

                // Then
                switch fulfillmentCount {
                case 0:
                    XCTAssertEqual(value, "o")
                case 1:
                    XCTAssertEqual(value, "old")
                default:
                    XCTFail()
                }

                promise.fulfill()
                fulfillmentCount += 1
            }
        }

        // When
        sendToServer("h")
        sendToServer("e")
        sendToServer("l")
        sendToServer("l")
        sendToServer("o")

        await sleep(2)

        sendToServer("wor")
        sendToServer("ld")

        wait(for: [promise], timeout: 10)
    }

    func testThrottler2() async throws {
        // Given
        let promise = expectation(description: "Ensure throttle before duration")
        let throttler = Limiter(policy: .throttle, duration: 1)

        var end = Date.now + 1
        promise.expectedFulfillmentCount = 2

        func test() {
            // Then
            XCTAssertLessThan(.now, end)
            promise.fulfill()
        }

        // When
        throttler(task: test)
        throttler(task: test)
        throttler(task: test)
        throttler(task: test)
        throttler(task: test)

        await sleep(2)
        end = .now + 1

        throttler(task: test)
        throttler(task: test)
        throttler(task: test)

        await sleep(2)

        wait(for: [promise], timeout: 10)
    }

    func testDebouncer2() async throws {
        // Given
        let promise = expectation(description: "Ensure debounce after duration")
        let debouncer = Limiter(policy: .debounce, duration: 1)

        var end = Date.now + 1
        promise.expectedFulfillmentCount = 2

        func test() {
            // Then
            XCTAssertGreaterThan(.now, end)
            promise.fulfill()
        }

        // When
        debouncer(task: test)
        debouncer(task: test)
        debouncer(task: test)
        debouncer(task: test)
        debouncer(task: test)

        await sleep(2)
        end = .now + 1

        debouncer(task: test)
        debouncer(task: test)
        debouncer(task: test)

        await sleep(2)

        wait(for: [promise], timeout: 10)
    }

    private func sleep(_ duration: TimeInterval) async {
        await Task.sleep(UInt64(duration * 1_000_000_000))
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM