簡體   English   中英

如何將 DispatchQueue 去抖動轉換為 Swift 並發任務?

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

我有一個使用DispatchQueue的現有 debouncer 實用程序。 它接受一個閉包並在達到時間閾值之前執行它。 它可以像這樣使用:

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來完成還是有更好的方法來做到這一點?

任務有能力使用isCancelledcheckCancellation ,但為了去抖動例程,你想等待一段時間,你可能只使用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)")
        }
    }
}

(為什么 Apple 恢復到納秒級是我無法理解的。)

請注意, sleep(nanoseconds:)的非拋出再現不會檢測到取消,因此您必須使用此拋出再現。

基於@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))
    }
}

測試在通過時不一致,所以我認為我對任務觸發順序的假設是不正確的,但我認為示例是一個好的開始:

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