防抖和节流
手写防抖和节流
简介
防抖(防止抖动)
- 操作暂停/结束过一段时间才发起请求。限制执行次数,多次密集的触发只执行一次。
- 应用场景:搜索时,当输入暂停时才发起请求,连续不停的输入时不发起请求。
节流
- 操作过程中,每间隔固定时间才发起请求。限制执行频率,有节奏的执行。
- 应用场景:在
drag
和scroll
时通常需要节流,以drag
为例,拖动过程中,并不需要一直发起请求,可使用节流以固定时间间隔才发起请求。
他们的使用场景通常不一样,但容易搞混淆。
以拖动为例,只要拖动就会离开原位置很多 px,不存在只拖动 1px,所以他是连续的、一次操作会执行多次的,就应该使用固定时间间隔来实现,以节省资源,这就需要用节流。
目的/背景
很多时候我们需要自己实现这些小的工具函数。既然有工具函数可以实现,为什么还要自己来重复造轮子来实现呢?
想象这样的场景:我们在开发一个组件,这个组件依赖了 lodash ,那我们把开发好的组件发布到 npm 仓库后,别人使用我们的仓库就需要安装 lodash,这个 lodash 和他原来安装的 lodash 可能版本不一样,导致他原来是用的版本失效,出现 bug。所以我们依赖越少越。github 中也有这样的去除工具函数的一些方法 去除lodash
实现
防抖
<input type="text" id="input" />
const input = document.getElementById("input");
input.addEventListener("keyup", () => {
console.log(input.value);
});
上面模拟普通的输入发请求(这里用打印替代)。
// 使用定时器实现防抖
const input = document.getElementById("input");
let timer = null;
input.addEventListener("keyup", () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
console.log(input.value);
timer = null;
}, 500)
});
当连续两次及以上操作时,第一次操作 timer 没值,将会赋值一个定时器返回值,将在 500 ms 后执行逻辑。
如果 500 ms 内有了第二次操作,这时候 timer 有值(是第一次赋值的),定时器将会被清除,第一步将要被执行的逻辑停止执行。
第二次 timer 将会被重新附上值,如果 500ms 内没有新的操作,就将执行内部逻辑。并把 timer 重置。下一次将会重新计算 timer 有没有值,重复上面步骤。
为了不每一个需要用到的地方都这样写一遍防抖逻辑,需要将其封装为一个方法,需要的地方直接调用。
// 封装这个 debounce 函数(方法)
// 传入的 fn 函数表示延迟一定时间后需要执行的逻辑
function debounce(fn, delay = 500) {
// timer 处于闭包中
let timer = null;
// 函数作为返回值,形成了闭包
return function() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn();
timer = null;
}, delay)
}
}
input.addEventListener(
"keyup",
debounce((e) => {
// 第一个参数(函数)如果有参数,这个参数将会传给 debounce 返回的函数
console.log(e.target);
console.log(input.value);
}, 1000)
);
节流
<!-- 给元素设置 draggable="true" 后,该元素便可以拖动了 -->
<div id="elem" class="elem" draggable="true">可拖拽元素</div>
elem.addEventListener("drag", (e) => {
console.log(e.offsetX, e.offsetY);
});
上面代码没有任何节流措施,当拖动元素时,将一直不停的打印信息。
let timer = null;
elem.addEventListener("drag", (e) => {
// 拖动第一 px 以外情况,400ms 以内的其他拖动,就什么都不做直接返回(这时候 timer 已经有值了)
// 400ms 以后,就会执行逻辑,并把 timer 重置,并循环上一次逻辑(又等待 400ms 再执行,并把 timer 重置)
if (timer) {
return;
}
timer = setTimeout(() => {
console.log(e.offsetX, e.offsetY);
timer = null;
}, 400);
});
同样的,我们可以封装成工具函数,供多个地方使用。
function throttle(fn, delay = 500) {
// timer 处于闭包中
let timer = null;
// 函数作为返回值,形成了闭包
return function () {
if (timer) {
return;
}
timer = setTimeout(() => {
// fn(); // 这样写接受不到 fn 函数中传入的参数
fn.apply(this, arguments);
timer = null;
}, delay);
};
}
elem.addEventListener(
"drag",
throttle((e) => {
// 第一个参数(函数)如果有参数,这个参数将会传给 throttle 返回的函数
console.log(e.offsetX, e.offsetY);
}, 200)
);
欢迎写出你的看法,一起成长!