防抖和节流
前言
¶防抖(debounce)和节流(throttle)在前端开发中十分常见,它们都是针对一个事件被连续触发时限制执行次数的算法,不同的是 debounce 只处理最后一次事件触发,而 throttle 则以一个固定的频率处理事件触发。你可以在这里直观地观察到它们之间的区别。
在开始正文之前,先看一下 Typescript 的两个工具类型 Parameters
和 ReturnType
,引入它们的目的是因为 debounce
和 throttle
的实现都以高阶函数的方式展现,本着作死无极限精益求精的态度,在实现时自然要考虑返回的函数和原函数应具有相同的参数类型的调用体验啦。
Parameters
: Typescript@3.1 引入的工具类型,用于获取一个函数的参数类型[1]:Parameters.d.ts12345type Parameters<T extends (...args: any[]) => any> =T extends (...args: infer P) => any ? P : nevertype A = Parameters<(x: number, y: number) => void>// A 的类型为 [x: number, y: number]ReturnType
: Typescript@2.8 引入的工具类型,用于获取一个函数的返回类型:ReturnType.d.ts12345type ReturnType<T extends (...args: any) => any> =T extends (...args: any) => infer R ? R : anytype A = ReturnType<(x: number, y: number) => string>// A 的类型为 string
防抖 (debounce)
¶防抖是指事件连续触发时,只在最后一次事件触发后再执行响应动作。比如,事件被触发的
适用场景:
- 提交表单时,快速点击多次,但只执行一次提交。
- 搜索框中输入内容实时展示关联条目,只在输入停顿时才发起查询请求(和
throttle
中列出的场景有所不同)。
简化版
¶在了解了 debounce 要解决的问题后,不难想到可以利用 setTimeout
来倒计时,当函数被顺利执行时,则重置定时器,表示当前处于空闲状态;否则,有一个新的事件在倒计时未结束时触发,则同样将计时器重置,但只开始计时,不执行任务。由此不难实现一个简单的版本:
123456789101112131415export function debounce<T extends (...args: any[]) => any>( fn: T, wait?: number,): (...args: Parameters<T>) => void { // 浏览器中 setTimeout 返回的是 number // 而 NodeJs 中返回的是 NodeJs.Timeout let timeout: ReturnType<typeof setTimeout> return debounced
function debounced(this: unknown, ...args: Parameters<T>): void { if (timeout) clearTimeout(timeout) const self = this timeout = setTimeout(() => fn.apply(self, args), wait) }}
注意上面代码的高亮部分,将 debounced
的 this
指针绑定到 fn
中再执行,这是
Javascript 老生常谈的问题了:当 fn
中通过 this
引用变量时,this
指针将默认指向 Window
[2],而在有些情况下,执行 fn
时会显式地绑定一个 this
指针,如 DOM 事件的回调函数中,会把触发事件的元素作为 this
绑定到 fn
中,于是我们可以通过 this
指针去访问当前触发事件的元素:
123document.body.addEventListener('click', debounce(function () { console.log(this.innerText)}))
带返回值
¶上面的实现版本中,debounced
返回的是 void
,对于大多数场景已经够用了,但如果要返回值的话,应该如何考虑呢?debounced
在事件连续多次触发时只会执行一次,我们可以记录下最后一次执行时的结果,然后在每次非实际执行时,返回上一次的结果:
123456789101112131415161718export function debounce<T extends (...args: any[]) => any>( fn: T, wait?: number,): (...args: Parameters<T>) => ReturnType<T> | void { let timeout: ReturnType<typeof setTimeout> let lastResult: ReturnType<T> | undefined return debounced
function debounced(...args: Parameters<T>): ReturnType<T> | void { if (timeout) clearTimeout(timeout)
const self = this timeout = setTimeout(function () { lastResult = fn.apply(self, args) }, wait) return lastResult }}
节流 (throttle)
¶节流是指事件连续触发时,在一个固定的时间间隔内只执行一次响应动作。比如事件被触发的
适用场景:
- UI 的拖拽事件、鼠标点击事件、滚动事件等,无需为每一次事件触发进行响应,但需要在一个时间范围内至少作出一次响应,否则会影响用户体验(掉帧)。
- 搜索框中输入内容实时展示关联条目,每隔一个固定的时间间隔(如
秒)发起一次查询请求(和debounce
中列出的场景有所不同)。
简化版
¶既然节流只需保证一个固定周期内只执行一次函数,则可以在空闲状态时接收事件触发,并在事件触发时启动一个倒计时的定时器,在倒计时未结束前,忽略所有的其它事件触发,等到上一次处理操作完成后,再将计时器重置,表明重新进入空闲状态。由此可以实现一个简化版的节流函数:
1234567891011121314151617export function throttle<T extends (...args: any[]) => any>( fn: T, wait?: number,): (...args: Parameters<T>) => void { let timeout: ReturnType<typeof setTimeout> | null return throttled
function throttled(...args: Parameters<T>): void { if (timeout) return
const self = this timeout = setTimeout(function () { fn.apply(self, args) timeout = null }, wait) }}
上面的代码中仍然考虑了 this
指针的指向问题,此处不再赘述。
带返回值
¶同样地,我们可以记录下上一次 throttled
返回值,在未实际执行的事件触发中,简单地返回一次记录的返回值。
123456789101112131415161718export function throttle<T extends (...args: any[]) => any>( fn: T, wait?: number,): (...args: Parameters<T>) => ReturnType<T> | void { let timeout: ReturnType<typeof setTimeout> | null let lastResult: ReturnType<T> | undefined return throttled
function throttled(...args: Parameters<T>): ReturnType<T> | void { if (timeout) return lastResult
const self = this timeout = setTimeout(function () { lastResult = fn.apply(self, args) timeout = null }, wait) }}
Related
¶