防抖(debounce)
防抖,顾名思义,防止抖动,以免把一次事件误认为多次,敲键盘就是一个每天都会接触到的防抖操作。
防抖的应用场景
- 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖。
- 调整浏览器窗口大小时,
resize
次数太过于频繁,造成计算过多,此时需要一次到位,就用到了防抖。 - 文本编辑器实时保存,当无任何更改操作一秒后进行保存。
特点
等待某种操作停止后,加以间隔进行操作
- 持续触发不执行
- 不触发的一段时间之后再执行
代码实现
代码如下,可以看出来防抖重在清零clearTimeout(timer)
function debounce(f, wait) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(()=>{
f(...args)
}, wait)
}
}
节流(throttle)
节流,顾名思义,控制水的流量。控制事件发生的频率,如控制为1秒发生一次,甚至1分钟发生一次。与服务端(server)及网关(gateway)控制的限流(Rate Limit)类似。
节流的应用场景
scroll
事件,每隔一秒计算一次位置信息等。- 浏览器播放事件,每隔一秒计算一次进度信息等。
input
框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求(也可做防抖)
特点
每等待某种间隔后,进行操作
- 持续触发并不会执行多次
- 到一定事件 / 其他间隔(如滑动的高度)再去执行
代码实现
代码如下,可以看出来节流重在开关锁timer=null
function throttle(f, wait) {
let timer
return (...args) => {
if(timer) { return }
timer = setTimeout(() => {
f(...args)
timer = null
}, wait)
}
}
如何实现节流防抖
在实际开发的业务场景中,我们更多的会选择成熟的第三方库来达到防抖和节流的效果。目前常用的有:Lodash
、Underscore.js
等,还可以使用计数器来实现节流防抖等。
防抖代码实现
<input name="username" type="text" oninput="checkUsername(this)"/>
<script>
var handler;
function checkUsername(it) {
clearTimeout(handler);
handler = setTimeout(() => {
// TODO
}, 1000)
}
</script>
上面代码的意思是:如果在1000毫秒内,反复执行checkUsername
函数,那么setTimeout
就会被反复清楚,并且重复设置一个新的1000毫秒的定时任务。直到你执行checkUsername
的前后两次间隔超过1000毫秒,定时任务才有机会执行一次。
节流代码实现
<input name="name" type="text" oninput="checkUsername(this)" />
<script>
var isRuning = false;
function checkUsername(it) {
if(!isRuning) {
isRuning = true;
// TODO
setTimeout(() => {
isRuning = false;
}, 1000)
}
}
</script>
上面代码的意思是:第一次执行checkUsername
函数的时候,!isRuning
为true
,执行关键代码,改变isRuning
为true
,设置一个1000毫秒后再把isRuning
再改为false
。那么,当你在1000毫秒内反复执行checkUsername
的话,关键代码都不会执行,只有等到定时器执行后,再次执行checkUsername
,才会执行关键代码。
Lodash防抖节流源码分析
防抖:Lodash
实现防抖的核心思想在于不去频繁管理定时器,而是实现了shouldInvoke
来判断是否应该执行func
函数,只有在对外提供的cancel
方法取消延迟时才取消定时器。
下文在函数执行模块详细介绍了shouldInvoke
内部实现逻辑,在定时器开关和入口函数中调用来决定是否应该执行func
函数。
基本定义
以下是Lodash
实现防抖的整体代码结构,入口函数定义了一些定时器相关和函数执行相关的变量。一共10个变量,其中maxWait
、timerId
、lastCallTime
、lastInvokeTime
、leading
、maxing
、trailing
7个时间相关的变量是实现定时器开关和函数执行模块的重要支撑。
import isObject from './isObject.js'
import roor from './internal/root.js'
function debounce(func, wait, options) {
/** ===== 基础定义 ===== **/
let lastArgs, // 上一次执行debounce的arguments
lastThis, // 上一次的 this
maxWait, // 最大等待时间,保证大于设置的最大间隔后一定会执行,用于实现节流效果
result, // 函数 func 执行后的返回值
timerId, // 定时器ID
lastCallTime // 上一次调用 debounce 的时间
let lastInvokeTime = 0 // 上一次执行func的时间,用于实现节流效果
let leading = false // 延迟前第一次触发
let maxing = false // 是否设置了最大等待时间maxWait,多用于实现节流效果
let trailing = true // 延迟后最后一次触发
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 隐式转换
wait = +wait || 0
/**
* isObject 判断是否是一个对象
* function isObject(value) {
* const type = typeof value
* return value != null && (type == 'object' || type == 'function')
* }
*/
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
// maxWait 取 maxWait 和 wait 中最大值,为实现节流效果,需保证 maxWait 的实际值大于 wait
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
/** ====== 定时器开关 ====== */
// 设置定时器
function startTimer(pendingFunc, wait) {}
// 取消定时器
function cancelTimer(id) {}
// 计算仍需等待的时间
function remainingWait(time) {}
// 定时器回调
function timerExpired() {}
/** ====== 函数执行 ====== */
// 延迟前
function leadingEdge(time) {}
// 延迟后回调
function trailingEdge(time) {}
// 执行 func 函数
function invokeFunc(time) {}
// 判断此时是否应该执行 func 函数
function shouldInvoke(time) {}
/** ====== 对外回调 ====== */
// 取消延迟
function cancel() {}
// 立即调用
function flush() {}
// 判断是否在定时中
function pending() {}
// 入口函数
function debounced(...args) {}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
export default debounce
定时器开关
/** ====== 定时器开关 ====== */
// 设置定时器
function startTimer(pendingFunc, wait) {
if (useRAF) {
// 没设置 wait 或设置 wait 为 0 时调用 window.requestAnimationFrame()。
// 要求浏览器在下次重绘之前调用指定的回调函数更新动画
root.cancelAnimationFrame(timerId)
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 取消定时器
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
// 计算仍需等待的时间
function remainingWait(time) {
// 当前时间与上一次调用 debounce 的间隔
const timeSinceLastCall = time - lastCallTime
// 当前时间与上一次执行 func 的间隔
const timeSinceLastInvoke = time - lastInvokeTime
// 剩余等待时间
const timeWaiting = wait - timeSinceLastCall
// 是否设置了最大等待时间 ( 是否设置为节流 )
// 否:剩余等待时间
// 是:剩余等待时间 和 当前时间与上一次执行 func 的间隔 中的最小值
return maxing ?
Math.min(timeWaiting, maxWait - timeSinceLastInvoke) :
timeWaiting
}
// 定时器回调
function timerExpired() {
const time = Date.now()
// 应该执行 func 函数时,执行延迟后回调
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 计算仍需等待的时间,重置定时器
timerId = startTimer(timerExpired, remainingWait(time))
}
函数执行
/** ====== 函数执行 ====== */
// 延迟前
function leadingEdge(time) {
// 设置上次执行 func 函数的时间
lastInvokeTime = time
// 设置定时器
timerId = startTimer(timerExpired, wait)
// 如果设置了 leading 则立即执行 func 函数一次
return leading ? invokeFunc(time) : result
}
// 延迟后回调
function trailingEdge(time) {
timerId = undefined
// trailing 延迟后继续触发一次
// lastArgs 标记着 debounce 至少执行过一次
if (trailing && lastArgs) {
return invokeFunc(time)
}
// 重置参数
lastArgs = lastThis = undefined
return result
}
// 执行 func 函数
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
// 判断此时是否应该执行 func 函数
function shouldInvoke(time) {
// 当前时间与上一次调用 debounce 的间隔
const timeSinceLastCall = time - lastCallTime
// 当前时间与上一次执行 func 的间隔
const timeSinceLastInvoke = time - lastInvokeTime
// 首次调用
// 超出等待时间间隔 wait
// 系统时间发生了变更
// 超出最长等待时间 maxWait
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
对外回调
/** ====== 对外回调 ====== */
// 取消延迟
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
}
节流:Lodash中节流函数的实现简洁,直接调用防抖函数,通过设置入参的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
}
return debounce(func, wait, {
leading,
trailing,
'maxWait': wait
})
}
export default throttle
总结
防抖:防止抖动,不触发的一段时间之后再执行。代码实现重在清零clearTimeout
节流:控制流量,单位时间内事件只能触发一次,如果服务器端的限流即Rate Limit。代码实现重在开关锁timer = timeout; timer = null
借用防抖和节流的思想,来控制函数执行的时机,可以节约性能,避免页面卡断等带来不好的用户体验。防抖和节流的概念相似不易区分。