函数函数防抖防抖是我们解决频繁触发DOM事件的两种常用解决方案。防抖和截流的应用场景有很多,输入框持续输入、将输入内容远程校验、多次触发点击事件、onScroll等等。
我们举个例子:我们在进行输入搜索的时候,我们会在一个输入框中输入我们想要的搜索的key,这个key会每次都像后端发送请求,获取搜索结果并显示。如果说我们输入速度特别快的话,就会有一种可能就是在很短时间内像后端发送很多的个请求,造成后端的接口的拥堵问题,另一方面每次搜索的结果的速度时不一样的,返回的速度也是有区别的,就是这些小小的区别会造成我们显示的结果并不是我们想要的等等问题。
解决类似于上面的问题就会用到防抖或者是节流。其实对于防抖和节流很多的库中都有实现,比如说lodash
还有就是有一个专门的防抖节流库throttle-debounce
函数防抖
概念
函数防抖(debounce),就是指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。
简单的实现
根据上面我们介绍的概念,我们先来简单的分析一下,防抖函数具备的那些特点,以及具体的实现思路:
防抖的具备的特点:
- 防抖函数肯定是一个函数,而且返回的结果也一定是一个函数
- 防抖函数的参数重一定要有一个执行函数和一个时间间隔
- 由于n秒内只能执行一次,需要有个执行的状态的记录情况,
- 如果n秒过去之后继续触发,就会再重新执行函数,这就说明需要有个定时器计时
好了根据上面的特点,我们来实现一个简单的防抖函数:
function debounce(fn,wait){
var timer = null; // 这里我们需要有一个标
return function(){
if(timer !== null){
clearTimeout(timer);
}
timer = setTimeout(fn,wait);
}
}
// 下面是具体的使用方法
function handle(){
console.log(Math.random());
}
window.addEventListener("resize",debounce(handle,1000));
简单实现的缺陷以及如何改造
上面的简单的实现乍一看没什么问题,但是实际使用的时候我们会面临着以下几个问题:
- 传入的函数执行的上下文(也就是我们在执行的函数中想要使用
this
,那么这this指的是什么) - 执行的函数中如果有参数,我们应该如何将参数传递下去(就上面的这个例子中,resize执行会有一个event参数,我们如何在传入的函数
handle
中使用这个参数呢?) - 当事件被第一次触发的时侯,我们希望能够立即执行一次我们传递的函数,应该如何去实现
- 如果我们设置的时间过长的话,我们需要怎么去进行取消
面对上面的问题,我们接下来对我们的防抖函数进行改造一下。
function debounce(func, wait, immediate) {
var timeout, result;
var later = function (context, args) {
timeout = null
result = func.apply(context, args);
};
var debounced = function () {
var context = this
var args = arguments
if (immediate) {
var callNow = !timeout;
if (callNow) {
later(context, args)
}
}
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(function () {
later(context, args)
}, wait)
return result
};
debounced.cancel = function () {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
函数防抖的使用场景
上面是一个防抖函数的一个实现,有了函数的防抖,但是函数防抖的使用场景有哪些?
- 输入框搜索自动补全事件,
- 鼠标/触摸屏的mouseover/touchmove事件
- 频繁操作点赞和取消点赞
- 频繁的提交数据等
函数节流
概念
函数的节流就是限制一个函数在一定时间内只能执行一次。
接下来我们根据概念具体分析函数节流具备的特点:
- 首先和防抖函数一样,都是一个函数,返回的结构也是一个函数
- 同样参数需要一个函数和时间间隔。
根据上面的这个点我们首先能能够想到的实现思路就是:
- 如果时间差大于了规定的等待时间,就可以执行一次;
- 目标函数执行以后,就更新 previous 值,确保它是“上一次”的时间。
- 否则就等下一次触发时继续比较。
接下来具体实现一下:
function throttle(func, wait) {
var previous = 0;
return function() {
var now = +new Date();
var context = this;
if (now - previous >= wait) {
func.apply(context, arguments);
previous = now; // 执行后更新 previous 值
}
}
}
没错,这是能够做到函数的节流的,但是这又同样出现一个问题,就是最后一次的问题,也就是终点问题。我们举个例子,比如说我们设置的wait是分钟,但是在59秒的时候触发了一次,这个时候函数是不会被执行的。会丢失一些我们想要的最终态。
为了解决上面的问题,我们稍微的改变一下我们的实现思路:
用定时器实现时间间隔。
- 当定时器不存在,说明可以执行函数,于是定义一个定时器来向任务队列注册目标函数
- 目标函数执行后设置保存定时器ID变量为空
- 当定时器已经被定义,说明已经在等待过程中。则等待下次触发事件时再进行查看。
具体的实现如下:
function throttle(func, wait) {
var time
return function(){
var context = this
if(!time){
time = setTimeout(function(){
func.apply(context, arguments)
time = null
}, wait)
}
}
}
接下来将他们进行细节的优化一下:
throttle = function(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};
函数节流的具体使用场景
到此为止,相信各位应该对函数节流有了一个比较详细的了解,那函数节流一般用在什么情况之下呢?
- 懒加载、滚动加载、加载更多或监听滚动条位置;
- 百度搜索框,搜索联想功能;
- 防止高频点击提交,防止表单重复提交;
- 鼠标的拖拽,move等相关的操作内容
目前遇到过的使用场景就是这些了,不过理解了原理,小伙伴可以把它运用在需要用到它的任何场合,提高执行效率。
总结
以上就是我个人对函数的防抖和节流的一个理解。在项目中有用到的地方,为了效率平时是需要多注意的。