防抖定义
某些频繁操作的事件会影响性能,"防抖"可以用来减少事件的响应频次,当事件触发的时候,相对应的函数并不会立即触发,而是会进行等待,只有等待了一段时间之后,事件停止触发,此时才会执行响应函数。
防抖案例
比如屏幕设定了1分钟的熄屏时间,如果用户在这1分钟之内,没有对电脑进行任何的操作,那么电脑就会在一分钟时准时熄屏,但如果用户在第50秒的时候,移动了一下鼠标,熄屏的时间会从这一刻开始重新计时,往后计算满1分钟没有进行任何操作时才会继续熄屏。
防抖使用场景
在程序设计的过程中,很多场景都能够用到"防抖"。
- 输入框频繁输入、搜索
- 按钮频繁点击、提交信息,触发事件
- 监听浏览器的滚动事件
- 监听浏览器的缩放事件
没有使用防抖时
这里模拟一个商品搜索框,我们需要对用户输入的内容调用接口进行关联查询,来给用户进行搜索提示。
当没有使用防抖时,我们会直接将函数绑定到对应的事件上。
// html代码
<input>
// js代码
const inputEl = document.querySelector('input')
let count = 0
const inputChange = function(){
console.log(`第${++count}次调用inputChange方法`)
}
inputEl.oninput = inputChange
input框内输入"javascript",一共10个字符,所以方法调用了10次
这样的方式性能很低,其一是因为每输入一个字符就调用接口,对服务器造成的压力很大,其二是因为直到第10个字符,用户才停止输入,此时才获取了用户输入的完整信息,前9个字符的调用就不是那么必要,那么,"防抖"的作用就是等到用户停止输入的时候,才去执行函数,避免了多次执行造成的资源浪费。
自定防抖函数
防抖函数实现的原理是,传入要执行的函数,以及需要等待推迟的时间,在经过一系列的处理之后,再去执行传入的函数。
第一步:基本版防抖实现
定义setTimeout推迟函数的执行时间,clearTimeout用于在输入下一个字符还没有超过1秒钟时清除计时器,创建新的计时器,如果没有清除的话,那么相当于每个字符都会隔1s调用方法。
const debounce = function (fn, delay) {
let timer = null;
const _debounce = function () {
if (timer) clearTimeout(timer);
timer = setTimeout(()=>{
fn()
}, delay);
};
return _debounce;
};
inputEl.oninput = debounce(inputChange, 1000);
我们是希望达到当用户输入完成时,再执行函数,也就是输入完10个字符,才调用一次
但此时发现,event.target.value 用于获取用户输入的值变成了undefined
第二步:拓展this和参数
在上一步定义函数时,this对象和event参数都被丢失了,在这里要把他们找回来,只需要在执行fn函数时,通过call/apply/bind来改变this的执行,以及传入参数即可。
const debounce = function (fn, delay) {
let timer = null;
const _debounce = function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(()=>{
fn.apply(this,args)
}, delay);
};
return _debounce;
};
inputEl.oninput = debounce(inputChange, 1000);
这样之后,我们就能拿到用户的输入内容啦~
到这里为止,已经实现了防抖的大部分使用场景,下面来看看更复杂的需求吧~
第三步:函数立即执行
在我们上面定义的防抖函数中,是没有立即执行的,也就是在输入第一个字符"j"的时候,是不会调用函数,可能存在一些场景,等到用户输入完成再调用显得响应比较缓慢,需要在第一个字符输入时就进行一次调用。
这里可以对于传入的函数增加一个参数,表示是否需要立即执行,默认不需要,为false,函数里在使用一个变量来保存是否需要首次执行,当首次执行完成后,再将这个变量置为false
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);
};
return _debounce;
};
inputEl.oninput = debounce(inputChange, 1000, true);
输入字符"j"时立即调用以及当用户输入完成时进行一次调用,所以这里会有两次调用。
第四步:取消功能
仔细一看,我们的防抖函数不能够取消呀,只要到了时间就一定会执行,万一当用户输完内容之后,还没有到1秒钟,但是他已经关掉窗口了呢,考虑到这种情况,我们得把取消功能安排上!
函数也是一个对象,我们可以给函数对象上再绑定一个函数,在return的_debounce上绑定一个cancel方法,当需要取消的时候执行cancel方法
// html
<button>取消</button>
// javascript
const cancelBtn = document.querySelector("button");
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;
};
const debounceFn = debounce(inputChange, 2000, true);
inputEl.oninput = debounceFn;
cancelBtn.onclick = debounceFn.cancel;
当输入了内容之后,在停止输入到点击按钮中没有超过定义的时间(上面定义的是2秒钟),这样就只会执行首次输入的函数,在用户输入完成的那一次就不会执行了。
第五步:函数返回值
上面定义的"防抖"函数是没有返回值的,如果说在执行完成之后,希望得到执行的结果,那么也有两种方式可以获取到
回调函数
在防抖函数的入参中增加第四个参数,是一个函数,用来获取防抖函数执行完成的结果
let count = 0;
const inputChange = function (event) {
console.log(`第${++count}次调用, 输入的内容为:${event?.target?.value}`);
return 'hello world'
};
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;
};
const debounceFn = debounce(inputChange, 2000, true, function(result){
console.log('获取执行的结果',result)
});
inputEl.oninput = debounceFn;
每执行一次防抖函数,都会执行回调函数里的内容。
promise
防抖函数的返回函数中,通过promise来返回成功或失败的结果,以下代码只判断了成功的执行条件,还可以加上失败的处理。
通过promise包裹的异步函数要经过调用才能获取响应结果,所以将防抖函数放在新函数中,将新函数作为oninput事件响应的函数。
const inputEl = document.querySelector("input");
let count = 0;
const inputChange = function (event) {
console.log(
`第${++count}次调用, 输入的内容为:${event?.target?.value}`
);
return "hello world";
};
const debounce = function (fn, delay, isImmediate = false) {
let timer = null;
let isExcute = isImmediate;
const _debounce = function (...args) {
return new Promise((resolve, reject) => {
if (isExcute) {
const result = fn.apply(this, args);
resolve(result);
isExcute = false;
}
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
const result = fn.apply(this, args);
resolve(result);
isExcute = isImmediate;
}, delay);
});
};
// 封装取消功能
_debounce.cancel = function () {
if (timer) clearTimeout(timer);
timer = null;
isInvoke = false;
};
return _debounce;
};
const debounceFn = debounce(inputChange, 2000, true);
const promiseCallback = function (...args) {
debounceFn.apply(inputEl, args).then((res) => {
console.log("promise的执行结果", res);
});
};
inputEl.oninput = promiseCallback;
和传入回调函数的形式一致,都是通过每执行一次防抖函数,就会返回一次结果。
在开发中使用防抖函数优化项目的性能,可以按如上方式自定义,也可以使用第三方库。
以上就是防抖函数相关内容,关于js高级,还有很多需要开发者掌握的地方,可以看看我写的其他博文,持续更新中~