昨天被问到这个问题,发现自己还不是很清楚。找了很多博客,学习一下。
防抖和节流是属于浏览器性能优化部分,事件的频繁触发,频繁操作dom,会导致页面卡顿和浏览器崩溃。但是,dom绑定的某些事件我们是控制不了它的触发频率
- window 的 resize()、scroll()
- mousemove
- mousedown、keydown
- 文字输入、自动完成的keyup事件,用户输入过程中事件不断的触发,会导致大量请求发出,但是响应跟不上。
对于window.resize()的处理,停止改变n毫秒后执行后续的处理,防抖 debounce
其他事件的需求,以一定频率执行后续的处理,期间不执行,节流 throttle
防抖 debounce
定义 : 策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。
简单实现的例子,滚动条监听
function debounce(fn, delay) {
let timer = null; // 借助闭包
console.log("借助闭包来实现")
return function () {
if (timer) {
// 正在计时的过程中,又触发了相同的事件,所以要取消当前计时,重新开始
clearTimeout(timer);
}
// 没有计时的存在,那就重新开始一个计时
timer = setTimeout(fn, delay);
}
}
function showTop() {
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
console.log("获取滚动位置", scrollTop);
}
// 实现 停止滚动1s后才能再次打印出位置
window.onscroll = debounce(showTop, 1000);
lodash 中的 debounce 函数: _.debounce(func, [wait=0], [options={}])
再贴一个lodash中的源码,有时间看看
function debounce(func, wait, options) {
var nativeMax = Math.max,
toNumber,
nativeMin
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
// func 上一次执行的时间
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;
// func必须是函数
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
// 对间隔时间的处理
wait = toNumber(wait) || 0;
// 对options中传入参数的处理
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
// 执行要被触发的函数
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
// 在leading edge阶段执行函数
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// 为 trailing edge 触发函数调用设定定时器
timerId = setTimeout(timerExpired, wait);
// leading = true 执行函数
return leading ? invokeFunc(time) : result;
}
// 剩余时间
function remainingWait(time) {
// 距离上次debounced函数被调用的时间
var timeSinceLastCall = time - lastCallTime,
// 距离上次函数被执行的时间
timeSinceLastInvoke = time - lastInvokeTime,
// 用 wait 减去 timeSinceLastCall 计算出下一次trailing的位置
result = wait - timeSinceLastCall;
// 两种情况
// 有maxing: 比较出下一次maxing和下一次trailing的最小值,作为下一次函数要执行的时间
// 无maxing: 在下一次trailing时执行timerExpired
return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
}
// 根据时间判断 func 能否被执行
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
// 几种满足条件的情况
return (lastCallTime === undefined // 首次执行
|| (timeSinceLastCall >= wait) // 距离上次被调用已经超过 wait
|| (timeSinceLastCall < 0)// 系统时间倒退
|| (maxing && timeSinceLastInvoke >= maxWait)); //超过最大等待时间
}
// 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器
function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// 重启定时器
timerId = setTimeout(timerExpired, remainingWait(time));
}
// 在trailing edge阶段执行函数
function trailingEdge(time) {
timerId = undefined;
// 有lastArgs才执行,
// 意味着只有 func 已经被 debounced 过一次以后才会在 trailing edge 执行
if (trailing && lastArgs) {
return invokeFunc(time);
}
// 每次 trailingEdge 都会清除 lastArgs 和 lastThis,目的是避免最后一次函数被执行了两次
// 举个例子:最后一次函数执行的时候,可能恰巧是前一次的 trailing edge,函数被调用,而这个函数又需要在自己时延的 trailing edge 触发,导致触发多次
lastArgs = lastThis = undefined;
return result;
}
// cancel方法
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
// flush方法--立即调用
function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
function debounced() {
var time = now(),
//是否满足时间条件
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time; //函数被调用的时间
// 无timerId的情况有两种:
// 1.首次调用
// 2.trailingEdge执行过函数
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
// 负责一种case:trailing 为 true 的情况下,在前一个 wait 的 trailingEdge 已经执行了函数;
// 而这次函数被调用时 shouldInvoke 不满足条件,因此要设置定时器,在本次的 trailingEdge 保证函数被执行
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
节流 throttle
定义 : 固定周期内,只允许执行一次,期间如再次触发,不执行。周期结束后,又有事件触发,开始新的周期。
例子 : 还是监听滚动条,如果用户一直拖着滚动条移动,使用debounce防抖不停止就不会打印信息。但是,用户想在这种情况下,也能够每个一段时间触发一下。
function throttle(fn, delay) {
let valid = true; // 这是一个状态位
return function () {
if (!valid) {
// 正在一个执行周期内,不允许再次执行
return false;
}
// 可以执行,并开始新的周期
valid = false;
setTimeout(() => {
fn(),
valid = true; // 新周期开始标志
}, delay)
}
}
function showTop() {
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
console.log("获取滚动位置", scrollTop);
}
// 实现 停止滚动1s后才能再次打印出位置
window.onscroll = throttle(showTop, 1000);
lodash 中提供的 throttle方法: _.throttle(func, [wait=0], [options={}])
应用场景
- 搜索框的input事件,需要支持输入的实时搜索,可以使用节流,设置每隔一段时间后,必须查询相关内容
- resize() 事件,常见于页面适配时,一般使用防抖,因为只需要处理最后一次的变化。
注意
- debounce 返回的函数必须是立即执行函数
function test() {
console.log(123)
}
setInterval(function () {
_.debounce(test, 1500)
}, 500)
// 每次setInterval执行后,返回函数没有执行
// 点击事件 这样写也不会生效
btn.addEventListener('click', function () {
_.debounce(test, 1500)
})
// 正确做法
btn.addEventListener('click', test)
setInterval(_.debounce(test, 1500), 500)
参考文章:
https://segmentfault.com/a/1190000018428170