為什么需要Throttle和Debounce
Throttle和Debounce在前端開(kāi)發(fā)可能比較經(jīng)常用到,做iOS開(kāi)發(fā)可能很多人不知道這個(gè)這個(gè)概念,其實(shí)很開(kāi)發(fā)者在工作中或多或少都遇到過(guò),就像設(shè)計(jì)模式有很多種,開(kāi)發(fā)中用到了某種設(shè)計(jì)模式自己卻不知道,這篇文章我們就簡(jiǎn)單聊Throttle和Debounce。
開(kāi)發(fā)中我們都遇到頻率很高的事件(如搜索框的搜索)或者連續(xù)事件(如UIScrollView的contentOffset進(jìn)行某些計(jì)算),這個(gè)時(shí)候?yàn)榱诉M(jìn)行性能優(yōu)化就要用到Throttle和Debounce。在詳細(xì)說(shuō)這連個(gè)概念之前我們先弄清楚一件事就是觸發(fā)事件和執(zhí)行事件對(duì)應(yīng)的方法是不同的。舉個(gè)栗子,有個(gè)button,我們點(diǎn)擊是觸發(fā)了點(diǎn)擊事件和之后比如進(jìn)行網(wǎng)絡(luò)這個(gè)方法是不一樣的,Throttle和Debounce并不會(huì)限制你去觸發(fā)點(diǎn)擊事件,但是會(huì)控制之后的方法調(diào)用,這和我們?cè)O(shè)置一種機(jī)制,去設(shè)置button的isEnable的方式是不同的。
Debounce
當(dāng)事件觸發(fā)超過(guò)一段時(shí)間之后才會(huì)執(zhí)行方法,如果在這段時(shí)間之內(nèi)有又觸發(fā)了這個(gè)時(shí)間,則重新計(jì)算時(shí)間。
電梯的處理就和這個(gè)類(lèi)似,比如現(xiàn)在在4樓,有個(gè)人按了1樓的按鈕(事件),這個(gè)時(shí)候電梯會(huì)等一固定時(shí)間,如果沒(méi)人再按按鈕,則電梯開(kāi)始下降(對(duì)應(yīng)的方法),如果有人立馬又按了1樓按鈕,電梯就會(huì)重新計(jì)算時(shí)間。
我們看看在面對(duì)search問(wèn)題上可以怎么處理
第一版
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)
}
}
這里運(yùn)用了DispatchWorkItem,將請(qǐng)求放在代碼塊中,當(dāng)有一個(gè)請(qǐng)求來(lái)時(shí)我們可以輕易的取消請(qǐng)求。正如你上面看到的,使用DispatchWorkItem在Swift中實(shí)際上比使用Timer或者Operation要好得多,這要?dú)w功于尾隨的閉包語(yǔ)法,以及GCD如何導(dǎo)入Swift。 你不需要@objc標(biāo)記的方法,或#selector,它可以全部使用閉包完成。
第二版
但只是這樣肯定不行的,我們?cè)囍シ庋b一下好在其他地方也能同樣使用。下面我們看看參考文章里的一個(gè)寫(xiě)法,當(dāng)然還有用Timer實(shí)現(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)
}
}
}
}
第三版
下面我們?cè)賹?duì)其進(jìn)行改進(jìn),一是使用DispatchWorkItem,二是使用DispatchSemaphore保證線(xiàn)程安全。
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
預(yù)先設(shè)定一個(gè)執(zhí)行周期,當(dāng)調(diào)用動(dòng)作大于等于執(zhí)行周期則執(zhí)行該動(dòng)作,然后進(jìn)入下一個(gè)新的時(shí)間周期
這有點(diǎn)像班車(chē)系統(tǒng)和這個(gè)類(lèi)似,比如一個(gè)班車(chē)每隔15分鐘發(fā)車(chē),有人來(lái)了就上車(chē),到了15分鐘就發(fā)車(chē),不管中間有多少乘客上車(chē)。
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ā)某個(gè)事件,那么就會(huì)造成一直無(wú)法調(diào)用相應(yīng)的方法,那么我們可以設(shè)置一個(gè)最大等待時(shí)間maxInterval,當(dāng)超過(guò)這個(gè)時(shí)間則執(zhí)行相應(yīng)的方法,避免一直等待。具體實(shí)施就不寫(xiě)了,讀者結(jié)合Debounce和Throttle可以自己去實(shí)現(xiàn),哈哈,這個(gè)有點(diǎn)像Debounce和Throttle的雜交品種。
參考文章