“浅入浅出”函数防抖(debounce)与节流(throttle)
函数防抖与节流是日常开发中经常用到的技巧,也是前端面试中的常客,但是发现自己工作一年多了,要么直接复用已有的代码或工具,要么抄袭《JS高级程序设计》书中所述“函数节流”,(实际上红宝书上的实现类似是函数防抖而不是函数节流),还没有认真的总结和亲自实现这两个方法,实在是一件蛮丢脸的事。网上关于这方面的资料简直就像是中国知网上的“水论文”,又多又杂,难觅精品,当然,本文也是一篇很水的文章,只当是个人理解顺便备忘,毕竟年纪大了,记忆力下降严重。CSS-Tricks上这篇文章Debouncing and Throttling Explained Through Examples算是非常通识的博文,值得一读。
函数防抖与节流的区别及应用场合
关于函数常规、防抖、节流三种执行方式的区别可以通过下面的例子直观的看出来
函数防抖和节流都能控制一段时间内函数执行的次数,简单的说,它们之间的区别及应用:
- 函数防抖: 将本来短时间内爆发的一组事件组合成单个事件来触发。等电梯就是一个非常形象的比喻,电梯不会立即上行,而是等待一段时间内没有人再上电梯了才上行,换句话说此时函数执行时一阵一阵的,如果一直有人上电梯,电梯就永远不会上行。
使用场合:用户输入关键词实时搜索,如果用户每输入一个字符就发请求搜索一次,就太浪费网络,页面性能也差;再比如缩放浏览器窗口事件;再再比如页面滚动埋点
- 函数节流: 控制持续快速触发的一系列事件每隔'X'毫秒执行一次,就像Magic把瓢泼大雨编程了绵绵细雨。
使用场合:页面滚动过程中不断统计离底部距离以便懒加载。
函数防抖与节流的简易实现
如果应用场合比较常规,根据上述函数防抖和节流的概念,代码实现还是比较简单的:
简易防抖工具函数实现如下:
function debounce(func, wait) {
let timerId
return function(...args) {
timerId && clearTimeout(timerId)
timerId = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
防抖高阶函数实现很简单,瞄一眼就懂,但是仍要注意:代码第三行返回的函数并没有使用箭头函数,目的是在事件执行时确定上下文,节流的高阶函数实现起来相对复杂一点。
function throttle(func, wait = 100) {
let timerId
let start = Date.now()
return function(...args) {
const curr = Date.now()
clearTimeout(timerId)
if (curr - start >= wait) {// 可以保证func一定会被执行
func.apply(this, args)
start = curr
} else {
timerId = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
}
Lodash函数防抖(debounce)与节流(throttle)源码精读
上面的基本实现大致满足绝大多数场景的需求,但是Lodash
库中的实现则更加完备,下面我们一起看看其源码实现。
import isObject from "./isObject.js"
import root from "./.internal/root.js"
function debounce(func, wait, options) {
/**
* maxWait 最长等待执行时间
* lastCallTime 事件上次触发的时间,由于函数防抖,真正的事件处理程序并不一定会执行
*/
let lastArgs, lastThis, maxWait, result, timerId, lastCallTime
let lastInvokeTime = 0 // 上一次函数真正调用的时间戳
let leading = false // 是否在等待时间的起始端触发函数调用
let maxing = false //
let trailing = true // 是否在等待时间的结束端触发函数调用
// 如果没有传入wait参数,检测requestAnimationFrame方法是否可以,以便后面代替setTimeout,默认等待时间约16ms
const useRAF =
!wait && wait !== 0 && typeof root.requestAnimationFrame === "function"
if (typeof func != "function") {
// 必须传入函数
throw new TypeError("Expected a function")
}
wait = +wait || 0 // wait参数转换成数字,或设置默认值0
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
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
// 开启计时器方法,返回定时器id
function startTimer(pendingFunc, wait) {
if (useRAF) {
// 如果没有传入wait参数,约16ms后执行
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 取消定时器
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
//等待时间起始端调用事件处理程序
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
// 事件上次触发到现在的经历的时间
const timeSinceLastCall = time - lastCallTime
// 事件处理函数上次真正执行到现在经历的时间
const timeSinceLastInvoke = time - lastInvokeTime
// 等待触发的时间
const timeWaiting = wait - timeSinceLastCall
// 如果用户设置了最长等待时间,则需要取最小值
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
// 判断某个时刻是否允许调用真正的事件处理程序
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
return (
lastCallTime === undefined || // 如果是第一次调用,则一定允许
timeSinceLastCall >= wait || // 等待时间超过设置的时间
timeSinceLastCall < 0 || // 当前时刻早于上次事件触发时间,比如说调整了系统时间
(maxing && timeSinceLastInvoke >= maxWait) // 等待时间超过最大等待时间
)
}
// 计时器时间到期执行的回调
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 重新启动计时器
timerId = startTimer(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
// 只有当事件至少发生过一次且配置了末端触发才调用真正的事件处理程序,
// 意思是如果程序设置了末端触发,且没有设置最大等待时间,但是事件自始至终只触发了一次,则真正的事件处理程序永远不会执行
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
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
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
}
export default debounce
Lodash中throttle
直接使用debounce
实现,说明节流可以当作防抖的一种特殊情况。
function throttle(func, wait, options) {
var leading = true,
trailing = true;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
});
}