blog
blog copied to clipboard
防抖debounce 与 节流throttle
防抖debounce 与 节流throttle
前言
一般对于监听某些密集型键盘、鼠标、手势事件需要和后端请求交互、修改 dom
的,防抖、节流就很有必要了。
防抖
使用场景
- 关键字远程搜索下拉框
- resize
对于这类操作,一般希望拿到用户最终输入的关键字、确定的拖拽大小,然后与服务器交互。 而中间态的值,并不关心,为了减轻服务器压力,避免服务器资源浪费,这时就需要防抖了。
案例
- 输入框防抖
// 记录时间
let last = Date.now();
//模拟一段ajax请求
function ajax(content) {
const d = Date.now();
const span = d - last;
console.log(`${content} 间隔 ${span}ms`);
last = d;
}
const noActionInput = document.getElementById('noAction');
noActionInput.addEventListener('keyup', function(e) {
ajax(e.target.value);
});
- 未防抖
可以看到为太多的中间态发送太多的请求。
/**
* 一般利用闭包存储和私有化定时器 `timer`
* 在 `delay` 时间内再次调用则清除未执行的定时器
* 重新定时器
* @param fn
* @param delay
* @returns {Function}
*/
function debounce(fn, delay) {
let timer = null;
return function() {
// 中间态一律清除掉
timer && clearTimeout(timer);
// 只需要最终的状态,执行
timer = setTimeout(() => fn.apply(this, arguments), delay);
};
}
const debounceInput = document.getElementById('debounce');
let debounceAjax = debounce(ajax, 100);
debounceInput.addEventListener('keyup', function(e) {
debounceAjax(e.target.value);
});
- 防抖后
可以发现:
- 如果输入的很慢,差不多每
delay 100ms
执行一次; - 如果输入的很快,说明用户在连续性输入,则会等到用户差不输入完慢下来了在执行回调。
节流
使用场景
- 滑动滚动条
- 射击类游戏发射子弹
- 水龙头的水流速
对于某些连续性的事件,为了表现平滑过渡,这时的中间态我们也需要关心的。 但减弱密集型事件的频率依旧是性能优化的杀器。
勘误
非常常见的两种错误写法,太流行了,忍不住出来勘误。
// 时间戳版
function throttleError1(fn, delay) {
let lastTime = 0;
return function () {
const now = Date.now();
const space = now - lastTime; // 时间间隔,首次会是很大一个值
if (space > delay) {
lastTime = now;
fn.apply(this, arguments);
}
};
}}
// 定时器版
function throttleError2(fn, delay) {
let timer;
return function () {
if (!timer) {
timer = setTimeout(() => {
timer = null;
fn.apply(this, arguments);
}, delay);
}
};
}
这两个版本都有的问题,先假设 delay=100ms
,假设定时器都是按时执行的。
- 时间戳版
- 由于首次
now - lastTime === now
该值很大,首次 0ms 立即执行,用户接在 0-100ms 内执行的交互均无效,假如用户停留在 99ms,则最后一次丢失了。 - 例如要用滚动条离顶部的高度来设置样式,滚动条在 99ms 从 0 滚动到 100px 处,你没办法处理。
-
PS: 时间戳版,有一个应用场景,在一定时间内防止重复提交。
-
定时器版
- 首次 0ms 不会立即执行有 100ms 延迟,好比开第一枪需要 100ms 后子弹才能出来。
- 聪明的读者,可能想到了,可以结合两者来解决问题。
- 首次 0ms 立即执行无延迟;
- 获取最后状态,保证最后一次得到执行。
案例
- 滚动滑动条时视觉上连续调整
dom
/**
* 时间戳来处理首次和间隔执行问题
* 定会器来确保最后一次状态改变得到执行
* @param fn
* @param delay
* @returns {Function}
*/
function throttle(fn, delay) {
let timer, lastTime;
return function() {
const now = Date.now();
const space = now - lastTime; // 间隔时间
if( lastTime && space < delay ) { // 为了响应用户最后一次操作
// 非首次,还未到时间,清除掉计时器,重新计时。
timer && clearTimeout(timer);
// 重新设置定时器
timer = setTimeout(() => {
lastTime = Date.now(); // 不要忘了记录时间
fn.apply(this, arguments);
}, delay);
return;
}
// 首次或到时间了
lastTime = now;
fn.apply(this, arguments);
};
}
const throttleAjax = throttle(ajax, 100);
window.addEventListener('scroll', function() {
const top = document.body.scrollTop || document.documentElement.scrollTop;
throttleAjax('scrollTop: ' + top);
});
- 节流前
回调过于密集。(PS:经常听到 scroll
自带节流,就是指一帧 16ms
左右触发一次)
- 节流后
可以发现,无论你滑的慢还是快都类似于定时触发。
- 细心的读者可能会发现,假如交互停留在 199ms,定时器在 300ms 段才执行,间隔了约 200ms,定时器延迟不应该设置为原来的
delay
。
/**
* 时间戳来处理首次和间隔执行问题
* 定会器来确保最后一次状态改变得到执行
* @param fn
* @param delay
* @returns {Function}
*/
function throttle(fn, delay) {
let timer, lastTime;
return function() {
const now = Date.now();
const space = now - lastTime; // 间隔时间
if( lastTime && space < delay ) { // 为了响应用户最后一次操作
// 非首次,还未到时间,清除掉计时器,重新计时。
timer && clearTimeout(timer);
// 重新设置定时器
timer = setTimeout(() => {
// lastTime = now; // (改:不准)
lastTime = Date.now(); // 不要忘了记录时间
fn.apply(this, arguments);
- }, delay);
+ }, delay - space);
return;
}
// 首次或到时间了
lastTime = now;
fn.apply(this, arguments);
+ // 当前已执行,清除掉计时器,不清除会有多余的中间执行
+ timer && clearTimeout(timer);
};
}
- 如果忘了最后清除
- 最终效果
-
查看
lodash
源码可以发现节流,是靠leading
来控制首次是否需要执行,trailing
来控制 99ms 停止 100ms时需不需要执行,maxWait
来控制定时执行,看完本篇去分析,是不是就很好理解了呢。 -
举几个常用的
lodash
使用方式。
- 实现类似
scroll
自带一帧16ms
效果:throttle(fn, /* wait = undefined */)
,其实内部用到了requestAnimationFrame
。 - 非常常用,发请求防止重复提交,例如首次点击执行,
500ms
内的点击一律不执行:debounce(fn, 500, { leading: true, trailing: false })
- 例如某个
dom
被清除,debounced.cancel()
来取消最后一次(trailing)调用,避免取不到 dom 报错。 - 等等。
总结
防抖、节流都是利用闭包来实现内部数据获取与维护。 防抖比较好理解,节流就需要稍微需要思考下。两者还是有区别的,就不要一错再错,粘贴传播问题代码啦。 防抖、节流对于频繁dom事件性能优化是不可或缺的手段。