JS中的防抖(debouncing)与节流(throttling)是用来控制一个函数在一定时间内执行的次数(频次),他俩个用处相近、但又不完全相同。
注: 文章中的示例可以点击跳转到codepen进行测试,同时也被同步到个人网站链接
出现原因
为什么会出现这俩个技巧呢?换句话说,为什么要控制函数执行的频次?我们看下面的动图,当我们在区域内进行滚动时,如果只是监听scroll事件就去执行函数的时候,函数在1s内被执行的次数要超过30次。
如果我们在回调函数中做大量运算或Dom操作,函数如此高的执行频次就会造成页面的卡顿。为了避免这种情况,防抖与节流就起到了至关作用
防抖函数
当在一段时间内事件被连续调用时,防抖函数会控制这段时间内函数只会被执行一次。其基本原理就是当函数被调用时设置一个
setTimout
定时器来延时去执行真正的函数。
延时结束后执行(trailing)
如上图,延时结束后执行是指在这段延时时间内,不再触发函数,则真正的函数才会被执行。其原理是在真正函数被执行之前,如果函数再次被调用,则重置这个定时器。这种延时结束后执行是最普通、使用最频繁的一种。例如:
- 当用户输入内容进行请求时,为避免无用的请求,当输入停止时进行请求。(对请求函数进行防抖控制)
- 监听窗口改变时,我们只需要计算最终的窗口大小即可。(监听resize时,对回调函数进行防抖控制)
我们也可以点击下方链接做一下测试,尝试不同频率的点击下方Click
,观察防抖函数带来的改变。
延时开始前执行(leading)
如上图,延时开始前执行是指在这段延时时间内,连续的触发函数,只会在最开始执行一次。其原理是设置延时器前执行一次真正的函数,这时定时器作用只是为了标识此次延时时间内不能执行函数。这种延时开始前执行使用场景比较少,例如:
- 当用户点击刷新按钮时,可以尽早的执行函数。因为前后执行其实是一样的效果。本质上只是为了防止用户疯狂刷新
同样,我们可以点击下方链接,通过不同频率的点击Click
进行测试。
节流函数
节流函数的原理其实和防抖函数基本相同,不同的是,节流函数会设置一个最长等待执行时间,也就是说节流函数控制在一定时间内函数一定会执行一次。
像之前的防抖函数(延时结束后执行),如果我们一直在触发事件,那延时器会一直处于重置状态,真正的函数永远不会被执行,而节流函数会保证在一定时间内,执行一次。想象一下,如果我们要实现触底加载功能,监听scroll事件,在滚动状态下,我们不仅要控制频次,还需要隔一段时间去检查距底部距离。 这时,我们就需要节流函数(throttle)来控制。
同样你可以点击下方链接进行左右侧的滚动测试,左侧因为使用debounce进行控制,只有当滚动停止时才会判断距底部距离,所以会造成卡顿的效果,而右边的通过throttle控制,体验上要好。
参考
附录
lodash的debounce源码解析
function debounce(func, wait, options) {
let lastArgs, // 记录上一次参数
lastThis, // 记录上一次this值
maxWait, // 最大等待时间, 用于节流函数(throttle)
result, // 要返回的结果
timerId, // 保存定时器
lastCallTime // 记录上一次触发时间
let lastInvokeTime = 0 // 记录上一次真正执行的时间
let leading = false // 是否 延时开始前执行的 标识
let maxing = false // 是否 传入maxWait 的标识
let trailing = true // 是否 延时结束后执行 的标识
// 通过设置wait = 0 可以跳过 requestAnimationFrame
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
// 传入的func不是函数 抛出错误
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0 // 转化成数值
// 如果options是对象,对参数进行赋值
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
//执行真正的函数
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
// 将上一次参数、this值赋值为空,这样trailingEdge阶段不会再次执行
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
// 开始定时器,优先使用requestAnimationFrame
function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId);
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 取消定时器
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
// 延时开始前阶段(leading阶段)
function leadingEdge(time) {
// 设置被调用时间
lastInvokeTime = time
// 开始设置定时器,进入延时结束后阶段(trailing阶段)
timerId = startTimer(timerExpired, wait)
// 如果传入的leading为true,代表延时开始前要执行,立即执行。
return leading ? invokeFunc(time) : result
}
// 计算剩余时间
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime // 距离上一次触发时间
const timeSinceLastInvoke = time - lastInvokeTime // 距离上一次执行时间
const timeWaiting = wait - timeSinceLastCall // 等待剩余时间
// 如果有maxWait,则选取timeWaiting 和 还需等待被执行的时间 最小值,
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
// 判断是否可以调用,有四种情况下返回true
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
// 1. lastCallTime为undefined,第一次被触发
// 2. 距离上一次被触发的时间 大于 wait,既此次触发是在延时结束后 trailing阶段。
// 3. 系统时间倒叙
// 4. 传入了maxWait,且距上次调用时间已经超过了最大等待时间,应该被执行
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
// 判断是否被再次触发啦,重置定时器,计算剩余的时间
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
// 如果可以调用,则进入延时结束后阶段(trailing阶段)
return trailingEdge(time)
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time))
}
// 延时结束后阶段(trailing阶段)
function trailingEdge(time) {
timerId = undefined
// 如果lastArgs有值代表再次被触发,因为lastArgs在debounced函数中被赋值
// 1. trailing为false,代表延时结束后不执行。
// 2. lastArgs为undefined,当leading传入true,会直接执行函数,将lastArgs设置空,
// 当且仅当debounced函数被触发一次,且leading为true时。
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
// 取消定时器,将闭包中的变量置位初始值
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
// 立即执行,进入延时结束后阶段。
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
// 是否已经处于延时阶段,定时器不为空则代表处于该阶段
function pending() {
return timerId !== undefined
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time // 每次被触发,重新对lastCallTime赋值
// 允许被调用
if (isInvoking) {
// 第一次被触发,定时器不存在,进入延时开始前阶段(leading阶段)
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
// 如果传了maxWait,跳过延时开始前阶段(leading阶段)
if (maxing) {
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
// 定时器不存在时,设置定时器
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
复制代码
lodash的throttle源码解析
/**
* 节流函数其实是调用防抖函数(debounce),传入maxWait,
* 我们也可以自己使用debounce函数来主动传入一个maxWait
*/
function throttle(func, wait, options) {
// 默认延时阶段前后都会执行函数
let leading = true
let trailing = true
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading
trailing = 'trailing' in options ? !!options.trailing : trailing
}
// 调用防抖函数(debounce)传入maxWait, maxWait === wait
// 不接受 options.maxWait 是因为maxWait大于、或小于wait都不合理。
return debounce(func, wait, {
leading,
trailing,
'maxWait': wait,
})
}
复制代码