為什么需要Throttle和Debounce
Throttle和Debounce在前端開發(fā)可能比較經(jīng)常用到周瞎,做iOS開發(fā)可能很多人不知道這個這個概念相赁,其實很開發(fā)者在工作中或多或少都遇到過,就像設計模式有很多種呵曹,開發(fā)中用到了某種設計模式自己卻不知道款咖,這篇文章我們就簡單聊Throttle和Debounce。
開發(fā)中我們都遇到頻率很高的事件(如搜索框的搜索)或者連續(xù)事件(如UIScrollView的contentOffset進行某些計算)奄喂,這個時候為了進行性能優(yōu)化就要用到Throttle和Debounce铐殃。在詳細說這連個概念之前我們先弄清楚一件事就是觸發(fā)事件和執(zhí)行事件對應的方法是不同的。舉個栗子跨新,有個button富腊,我們點擊是觸發(fā)了點擊事件和之后比如進行網(wǎng)絡這個方法是不一樣的,Throttle和Debounce并不會限制你去觸發(fā)點擊事件玻蝌,但是會控制之后的方法調(diào)用蟹肘,這和我們設置一種機制词疼,去設置button的isEnable的方式是不同的俯树。
Debounce
當事件觸發(fā)超過一段時間之后才會執(zhí)行方法帘腹,如果在這段時間之內(nèi)有又觸發(fā)了這個時間,則重新計算時間许饿。
電梯的處理就和這個類似阳欲,比如現(xiàn)在在4樓,有個人按了1樓的按鈕(事件)陋率,這個時候電梯會等一固定時間球化,如果沒人再按按鈕,則電梯開始下降(對應的方法)瓦糟,如果有人立馬又按了1樓按鈕筒愚,電梯就會重新計算時間。
我們看看在面對search問題上可以怎么處理
第一版
class SearchViewController: UIViewController, UISearchBarDelegate {
// We keep track of the pending work item as a property
private var pendingRequestWorkItem: DispatchWorkItem?
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// Cancel the currently pending item
pendingRequestWorkItem?.cancel()
// Wrap our request in a work item
let requestWorkItem = DispatchWorkItem { [weak self] in
self?.resultsLoader.loadResults(forQuery: searchText)
}
// Save the new work item and execute it after 250 ms
pendingRequestWorkItem = requestWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
execute: requestWorkItem)
}
}
這里運用了DispatchWorkItem菩浙,將請求放在代碼塊中巢掺,當有一個請求來時我們可以輕易的取消請求。正如你上面看到的劲蜻,使用DispatchWorkItem在Swift中實際上比使用Timer或者Operation要好得多陆淀,這要歸功于尾隨的閉包語法,以及GCD如何導入Swift先嬉。 你不需要@objc標記的方法轧苫,或#selector,它可以全部使用閉包完成疫蔓。
第二版
但只是這樣肯定不行的含懊,我們試著去封裝一下好在其他地方也能同樣使用。下面我們看看參考文章里的一個寫法衅胀,當然還有用Timer實現(xiàn)的绢要,讀者感興趣可以自己看看
typealias Debounce<T> = (_ : T) -> Void
func debounce<T>(interval: Int, queue: DispatchQueue, action: @escaping Debounce<T>) -> Debounce<T> {
var lastFireTime = DispatchTime.now()
let dispatchDelay = DispatchTimeInterval.milliseconds(interval)
return { param in
lastFireTime = DispatchTime.now()
let dispatchTime: DispatchTime = DispatchTime.now() + dispatchDelay
queue.asyncAfter(deadline: dispatchTime) {
let when: DispatchTime = lastFireTime + dispatchDelay
let now = DispatchTime.now()
if now.rawValue >= when.rawValue {
action(param)
}
}
}
}
第三版
下面我們再對其進行改進,一是使用DispatchWorkItem拗小,二是使用DispatchSemaphore保證線程安全重罪。
class Debouncer {
public let label: String
public let interval: DispatchTimeInterval
fileprivate let queue: DispatchQueue
fileprivate let semaphore: DispatchSemaphoreWrapper
fileprivate var workItem: DispatchWorkItem?
public init(label: String, interval: Float, qos: DispatchQoS = .userInteractive) {
self.interval = .milliseconds(Int(interval * 1000))
self.label = label
self.queue = DispatchQueue(label: "com.farfetch.debouncer.internalqueue.\(label)", qos: qos)
self.semaphore = DispatchSemaphoreWrapper(withValue: 1)
}
public func call(_ callback: @escaping (() -> ())) {
self.semaphore.sync { () -> () in
self.workItem?.cancel()
self.workItem = DispatchWorkItem {
callback()
}
if let workItem = self.workItem {
self.queue.asyncAfter(deadline: .now() + self.interval, execute: workItem)
}
}
}
}
public struct DispatchSemaphoreWrapper {
private let semaphore: DispatchSemaphore
public init(withValue value: Int) {
self.semaphore = DispatchSemaphore(value: value)
}
public func sync<R>(execute: () throws -> R) rethrows -> R {
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
defer { semaphore.signal() }
return try execute()
}
}
Throttle
預先設定一個執(zhí)行周期,當調(diào)用動作大于等于執(zhí)行周期則執(zhí)行該動作哀九,然后進入下一個新的時間周期
這有點像班車系統(tǒng)和這個類似剿配,比如一個班車每隔15分鐘發(fā)車,有人來了就上車阅束,到了15分鐘就發(fā)車呼胚,不管中間有多少乘客上車。
import UIKit
import Foundation
public class Throttler {
private let queue: DispatchQueue = DispatchQueue.global(qos: .background)
private var job: DispatchWorkItem = DispatchWorkItem(block: {})
private var previousRun: Date = Date.distantPast
private var maxInterval: Int
fileprivate let semaphore: DispatchSemaphoreWrapper
init(seconds: Int) {
self.maxInterval = seconds
self.semaphore = DispatchSemaphoreWrapper(withValue: 1)
}
func throttle(block: @escaping () -> ()) {
self.semaphore.sync { () -> () in
job.cancel()
job = DispatchWorkItem(){ [weak self] in
self?.previousRun = Date()
block()
}
let delay = Date.second(from: previousRun) > maxInterval ? 0 : maxInterval
queue.asyncAfter(deadline: .now() + Double(delay), execute: job)
}
}
}
private extension Date {
static func second(from referenceDate: Date) -> Int {
return Int(Date().timeIntervalSince(referenceDate).rounded())
}
}
示例
import UIKit
public class SearchBar: UISearchBar, UISearchBarDelegate {
/// Throttle engine
private var throttler: Throttler? = nil
/// Throttling interval
public var throttlingInterval: Double? = 0 {
didSet {
guard let interval = throttlingInterval else {
self.throttler = nil
return
}
self.throttler = Throttler(seconds: interval)
}
}
/// Event received when cancel is pressed
public var onCancel: (() -> (Void))? = nil
/// Event received when a change into the search box is occurred
public var onSearch: ((String) -> (Void))? = nil
public override func awakeFromNib() {
super.awakeFromNib()
self.delegate = self
}
// Events for UISearchBarDelegate
public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
self.onCancel?()
}
public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
self.onSearch?(self.text ?? "")
}
public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
guard let throttler = self.throttler else {
self.onSearch?(searchText)
return
}
throttler.throttle {
DispatchQueue.main.async {
self.onSearch?(self.text ?? "")
}
}
}
}
思考
根據(jù)Debounce我們知道如果一直去觸發(fā)某個事件息裸,那么就會造成一直無法調(diào)用相應的方法蝇更,那么我們可以設置一個最大等待時間maxInterval沪编,當超過這個時間則執(zhí)行相應的方法,避免一直等待年扩。具體實施就不寫了蚁廓,讀者結合Debounce和Throttle可以自己去實現(xiàn),哈哈厨幻,這個有點像Debounce和Throttle的雜交品種相嵌。
參考文章