原理:当持续触发一个事件时,在n秒内,事件没有再次触发,此时才会执行回调;如果n秒内,又触发了事件,就重新计时
适用场景:
辅助理解:在你坐电梯时,当一直有人进电梯(连续触发),电梯门不会关闭,在一定时间间隔内没有人进入(停止连续触发)才会关闭。

<body>
<!-- 输入框 -->
不防抖输入框<input>
<script>
//获取输入框
const inputEl = document.querySelector('input')
//触发的函数
let count = 0
const inputChange = function(){
console.log(`第${++count}次调用inputChange方法`)
}
//输入触发
inputEl.oninput = inputChange
</script>
</body>
防抖函数实现的原理是,传入要执行的函数,以及需要等待推迟的时间,在经过一系列的处理之后,再去执行传入的函数。
定义setTimeout推迟函数的执行时间,clearTimeout用于在输入下一个字符还没有超过1秒钟时清除计时器,创建新的计时器,如果没有清除的话,那么相当于每个字符都会隔1s调用方法。

<body>
<!--
定义setTimeout推迟函数的执行时间,clearTimeout用于在输入下一个字符还没有超过1秒钟时清除计时器,创建新的计时器,
如果没有清除的话,那么相当于每个字符都会隔1s调用方法。
-->
基础版防抖<input>
<script>
const inputEl = document.querySelector('input')
let count = 0
const inputChange = function(){
console.log(`第${++count}次调用inputChange方法`)
}
//基础防抖函数
const debounce = function (fn, delay) {
//定时器变量
let timer = null;
const _debounce = function () {
//定时器不为null清除定时器
if (timer) clearTimeout(timer);
//定时器为空重新创建定时器
timer = setTimeout(()=>{
//执行回调函数
fn()
}, delay);
};
return _debounce;
};
inputEl.oninput = debounce(inputChange, 1000);
</script>
</body>
在上一步定义函数时,this对象和event参数都被丢失了,在这里要把他们找回来,只需要在执行fn函数时,通过call/apply/bind来改变this的执行,以及传入参数即可。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>扩展this和参数</title>
</head>
<body>
扩展this和参数<input>
<script>
const inputEl = document.querySelector('input')
let count = 0
//添加
const inputChange = function(e){
console.log(`第${++count}次调用,输入内容为${e.target.value}`)
}
const debounce = function (fn, delay) {
let timer = null;
const _debounce = function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(()=>{
//通过call/apply/bind 指定this,以及传入的事件对象
fn.apply(this,args)
}, delay);
};
return _debounce;
};
inputEl.oninput = debounce(inputChange, 1000);
</script>
</body>
</html>
在我们上面定义的防抖函数中,是没有立即执行的,也就是在输入第一个字符"j"的时候,是不会调用函数,可能存在一些场景,等到用户输入完成再调用显得响应比较缓慢,需要在第一个字符输入时就进行一次调用。
这里可以对于传入的函数增加一个参数,表示是否需要立即执行,默认不需要,为false,函数里在使用一个变量来保存是否需要首次执行,当首次执行完成后,再将这个变量置为false

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>函数立即执行</title>
</head>
<body>
函数立即执行<input>
<script>
const inputEl = document.querySelector('input')
let count = 0
const inputChange = function(e){
console.log(`第${++count}次调用,输入内容为${e.target.value}`)
}
const debounce = function (fn, delay, isImmediate = false) {
let timer = null;
//立即执行配置变量,默认为false:不立即执行
let isExcute = isImmediate;
const _debounce = function (...args) {
//判断立即执行
if (isExcute) {
//执行完一次后立即关闭
fn.apply(this, args);
isExcute = false;
}
if (timer) clearTimeout(timer);
timer = setTimeout(()=>{
//通过call/apply/bind 指定this,以及传入的事件对象
fn.apply(this,args)
isExcute = isImmediate;
}, delay);
};
return _debounce;
};
//配置立即执行
inputEl.oninput = debounce(inputChange, 1000, true);
</script>
</body>
</html>
仔细一看,我们的防抖函数不能够取消呀,只要到了时间就一定会执行,万一当用户输完内容之后,还没有到1秒钟,但是他已经关掉窗口了呢,考虑到这种情况,我们得把取消功能安排上!
函数也是一个对象,我们可以给函数对象上再绑定一个函数,在return的_debounce上绑定一个cancel方法,当需要取消的时候执行cancel方法

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>函数立即执行</title>
</head>
<body>
函数立即执行<input> <br>
<button>取消</button>
<script>
const inputEl = document.querySelector('input')
const cancelBtn = document.querySelector("button");
let count = 0
const inputChange = function(e){
console.log(`第${++count}次调用,输入内容为${e.target.value}`)
}
const debounce = function (fn, delay, isImmediate = false) {
let timer = null;
let isExcute = isImmediate;
const _debounce = function (...args) {
if (isExcute) {
fn.apply(this, args);
isExcute = false;
}
if (timer) clearTimeout(timer);
timer = setTimeout(()=>{
fn.apply(this,args)
isExcute = isImmediate;
}, delay);
};
//这里使用到了闭包的性质
_debounce.cancel = function () {
if (timer) clearTimeout(timer);
};
return _debounce;
};
debounceFn = debounce(inputChange, 1000, true);
//取消调用
inputEl.oninput = debounceFn;
cancelBtn.onclick = debounceFn.cancel;
</script>
</body>
</html>
上面定义的"防抖"函数是没有返回值的,如果说在执行完成之后,希望得到执行的结果,那么也有两种方式可以获取到
回调函数
在防抖函数的入参中增加第四个参数,是一个函数,用来获取防抖函数执行完成的结果

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>函数返回值</title>
</head>
<body>
函数返回值<input>
<script>
const inputEl = document.querySelector('input')
let count = 0
const inputChange = function (e) {
console.log(`第${++count}次调用,输入内容为${e.target.value}`);
return '执行结果';
}
const debounce = function (fn, delay, isImmediate = false, callbackFn) {
let timer = null;
let isExcute = isImmediate;
const _debounce = function (...args) {
if (isExcute) {
const result = fn.apply(this, args);
callbackFn(result)
isExcute = false;
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
//调用回调传入结果
const result = fn.apply(this, args)
callbackFn(result)
isExcute = isImmediate;
}, delay);
};
return _debounce;
};
//回调接收result
const debounceFn = debounce(inputChange, 1000, true, function (result) {
console.log('获取执行的结果', result)
});
inputEl.oninput = debounceFn;
</script>
</body>
</html>
防抖函数的返回函数中,通过promise来返回成功或失败的结果,以下代码只判断了成功的执行条件,还可以加上失败的处理。
通过promise包裹的异步函数要经过调用才能获取响应结果,所以将防抖函数放在新函数中,将新函数作为oninput事件响应的函数。

在开发中使用防抖函数优化项目的性能,可以按如上方式自定义,也可以使用第三方库。
https://www.lodashjs.com/docs/lodash.debounce#_debouncefunc-wait0-options