为什么需要Throttle和Debounce
Throttle和Debounce在前端开发可能比较经常用到,做iOS开发可能很多人不知道这个这个概念,其实很开发者在工作中或多或少都遇到过,就像设计模式有很多种,开发中用到了某种设计模式自己却不知道,这篇文章我们就简单聊Throttle和Debounce。
开发中我们都遇到频率很高的事件(如搜索框的搜索)或者连续事件(如UIScrollView的contentOffset进行某些计算),这个时候为了进行性能优化就要用到Throttle和Debounce。在详细说这连个概念之前我们先弄清楚一件事就是触发事件和执行事件对应的方法是不同的。举个栗子,有个button,我们点击是触发了点击事件和之后比如进行网络这个方法是不一样的,Throttle和Debounce并不会限制你去触发点击事件,但是会控制之后的方法调用,这和我们设置一种机制,去设置button的isEnable的方式是不同的。
Debounce
当事件触发超过一段时间之后才会执行方法,如果在这段时间之内有又触发了这个时间,则重新计算时间。
电梯的处理就和这个类似,比如现在在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实现的,读者感兴趣可以自己看看
typealias Debounce = (_ : T) -> Void
func debounce(interval: Int, queue: DispatchQueue, action: @escaping Debounce) -> Debounce {
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(execute: () throws -> R) rethrows -> R {
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
defer { semaphore.signal() }
return try execute()
}
}
Throttle
预先设定一个执行周期,当调用动作大于等于执行周期则执行该动作,然后进入下一个新的时间周期
这有点像班车系统和这个类似,比如一个班车每隔15分钟发车,有人来了就上车,到了15分钟就发车,不管中间有多少乘客上车。
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 ?? "")
}
}
}
}
思考
根据Debounce我们知道如果一直去触发某个事件,那么就会造成一直无法调用相应的方法,那么我们可以设置一个最大等待时间maxInterval,当超过这个时间则执行相应的方法,避免一直等待。具体实施就不写了,读者结合Debounce和Throttle可以自己去实现,哈哈,这个有点像Debounce和Throttle的杂交品种。
参考文章
- How can I debounce a method call?
- Throttling calls in Swift