一、防抖函数的使用场景及类型
- 使用场景:用于高频触发且有一定停顿的情况,单位时间内事件触发则等待时间会被重置,如:用户在短时间内多次点击登陆、搜索框根据输入的一部分值进行联想搜索(也可以使用节流)、短信验证码、resize等。
- 类型:防抖函数分为立即执行和非立即执行两种,立即执行的为前缘防抖,非立即执行的为延迟防抖。
二、防抖函数
- 前缘防抖
下面给了两种实现方式
使用定时器实现:
// 前缘防抖(定时器版本),在一定时间间隔内的连续触发只执行首次
function debounceImmediateExecution(fn, delay){
let timer = null;
return function (){
let args = [...arguments];
if(!timer){ // 首次触发或间隔 delay 时间后触发,立即执行 fn
fn.apply(this, args);
// 设置定时器
timer = setTimeout(function (){
timer = null;
}, delay);
}else{ // 在间隔时间内触发
// 取消旧的定时器
clearTimeout(timer);
// 设置新的延时定时器
timer = setTimeout(function (){
fn.apply(this, args);
timer = null;
}, delay)
}
}
}
使用时间戳实现:
// 前缘防抖(时间戳版本,比使用定时器开销更低),在一定时间间隔内的连续触发只执行首次
function debounceImmediateExecutionPlus(fn, delay){
let last = Date.now();
let first = true; // 是否为首次执行
return function (){
let args = [...arguments];
if(first){ // 首次触发
fn.apply(this, args);
last = Date.now();
first = false;
}else{ // 后续触发
let now = Date.now();
if(now - last >= delay){
fn.apply(this, args);
}
last = now;
}
}
}
- 延迟防抖
// 延迟防抖,在一定时间间隔内的连续触发只执行最后一次
function debounceDelayExecution(fn, delay){
let timer = null;
return function (){ // 最后将这个闭包函数返回作为包装后的事件监听函数
clearTimeout(timer); // 取消旧的定时器
let _this = this; // this 指向监听的节点,此处不保存的话到了定时器回调函数中 this 就会变为 window
let args = [...arguments]; // 事件监听函数的参数
// 重置定时器
timer = setTimeout(function (){
fn.apply(_this, args);
}, delay)
}
}
// 个人理解:延迟防抖最好用定时器实现,因为需要在满足某个条件后,让 fn 在经过 delay 时间后执行
- 可选前缘或延迟防抖
// 防抖完整版(相当于前缘防抖和延迟防抖都使用定时器实现时的结合),可选前缘防抖(默认)或者延迟防抖
function debounce(fn, delay, isImmediate = true){
let timer = null;
return function (){
let args = [...arguments];
let _this = this; // 保存 this 供后续操作中使用
if(timer){ // 已有定时器时,定时器需要重置,代表对中途连续触发的处理
clearTimeout(timer); // 取消旧的定时器
if(isImmediate){ // 使用前缘防抖时
timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
timer = null; // 执行后置空
}, delay);
}else{ // 使用延迟防抖时
timer = setTimeout(function (){ // 创建新的定时器
fn.apply(_this, args);
timer = null; // 执行后置空
}, delay);
}
}else{ // 没有定时器时,代表对首次触发或者间隔时间>=delay时的触发进行处理
if(isImmediate){ // 使用前缘防抖时
fn.apply(this, args)
timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
timer = null;
}, delay);
}else{ // 使用延迟防抖时
timer = setTimeout(function (){ // 创建新的定时器
fn.apply(_this, args);
timer = null; // 执行后置空
}, delay);
}
}
}
}
三、完整使用示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>防抖</title>
</head>
<body>
<div>
<input type="button" value="点我次数加1" id = 'button'>
<div>
<span>
当前点击次数为:
</span>
<span id = 'show'>
0
</span>
</div>
</div>
<script>
/**
* 防抖函数
*/
// 延迟防抖,在一定时间间隔内的连续触发只执行最后一次
function debounceDelayExecution(fn, delay){
let timer = null;
return function (){ // 最后将这个闭包函数返回作为包装后的事件监听函数
clearTimeout(timer); // 取消旧的定时器
let _this = this; // this 指向监听的节点,此处不保存的话到了定时器回调函数中 this 就会变为 window
let args = [...arguments]; // 事件监听函数的参数
// 重置定时器
timer = setTimeout(function (){
fn.apply(_this, args);
}, delay)
}
}
// 个人理解:延迟防抖最好用定时器实现,因为需要在满足某个条件后,让 fn 在经过 delay 时间后执行
// 前缘防抖(定时器版本),在一定时间间隔内的连续触发只执行首次
function debounceImmediateExecution(fn, delay){
let timer = null;
return function (){
let args = [...arguments];
if(!timer){
fn.apply(this, args);
// 设置定时器
timer = setTimeout(function (){
timer = null;
}, delay);
}else{
// 取消旧的定时器
clearTimeout(timer);
// 设置新的延时定时器
timer = setTimeout(function (){
timer = null;
}, delay)
}
}
}
// 前缘防抖(时间戳版本,比使用定时器开销更低),在一定时间间隔内的连续触发只执行首次
function debounceImmediateExecutionPlus(fn, delay){
let last = Date.now();
let first = true; // 是否为首次执行
return function (){
let args = [...arguments];
if(first){ // 首次触发
fn.apply(this, args);
last = Date.now();
first = false;
}else{ // 后续触发
let now = Date.now();
if(now - last >= delay){
fn.apply(this, args);
}
last = now;
}
}
}
// 防抖完整版(相当于前缘防抖和延迟防抖都使用定时器实现时的结合),可选前缘防抖(默认)或者延迟防抖
function debounce(fn, delay, isImmediate = true){
let timer = null;
return function (){
let args = [...arguments];
let _this = this; // 保存 this 供后续操作中使用
if(timer){ // 已有定时器时,定时器需要重置,代表对中途连续触发的处理
clearTimeout(timer); // 取消旧的定时器
if(isImmediate){ // 使用前缘防抖时
timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
timer = null; // 执行后置空
}, delay);
}else{ // 使用延迟防抖时
timer = setTimeout(function (){ // 创建新的定时器
fn.apply(_this, args);
timer = null; // 执行后置空
}, delay);
}
}else{ // 没有定时器时,代表对首次触发或者间隔时间>=delay时的触发进行处理
if(isImmediate){ // 使用前缘防抖时
fn.apply(this, args)
timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
timer = null;
}, delay);
}else{ // 使用延迟防抖时
timer = setTimeout(function (){ // 创建新的定时器
fn.apply(_this, args);
timer = null; // 执行后置空
}, delay);
}
}
}
}
/**
* 下面是过程的处理,节点获取,注册事件监听,函数封装等
*/
let times = 0; // 计数
let button = document.getElementById('button');
let show = document.getElementById('show');
// 实际(最初)的点击事件处理函数,可以在这里进行网络请求之类的操作
function handleClick(){
++times;
// 这里是替换显示的文本,使用 textContent 比 innerHTML 更安全
show.textContent = times;
}
// 将最初的事件处理函数进行包装,把 handleClick 进行防抖处理,返回闭包函数作为新的事件处理函数
// let packingHandleClick = debounceDelayExecution(handleClick, 2000);
// let packingHandleClick = debounceImmediateExecution(handleClick, 2000);
// let packingHandleClick = debounceImmediateExecutionPlus(handleClick, 2000);
let packingHandleClick = debounce(handleClick, 2000);
// 注册事件监听器
button.addEventListener("click", packingHandleClick);
</script>
</body>
</html>
想了解节流的朋友们可以看看我的另一篇文章:节流函数及其使用示例