前言
首先我们先来看看防抖和节流函数,来了解一下为啥要用到这两个函数,而且在面试中,这两个知识点也是经常会问到的地方,最后我会来和大家看一个我在实际中遇到的因为防抖函数导致的一个问题,和解决方法!
一、防抖
使用场景:持续触发事件,会等到停止后一段时间后才执行。
- 表单验证
- 搜索框输入查询
- 滚动条滚动
- 按键提交
我们设想一个场景,当我们有一个输入框时,我们输入框下方的内容是会根据我们输入的内容而进行更新和展示的。那么我们是不是需要监听输入框内的内容,当内容发生改变的时候,我们就要调用我们获取内容的方法。如下面的代码展示:
//这里我们用个鼠标滑过一个盒子来演示这个过程
<!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>Document</title>
<style>
body{
padding: 0;
margin: 0;
}
#but{
position:absolute;
top:0;
bottom:0;
left:0;
right:0;
margin:auto;
background-color:rgba(27, 191, 241, 0.2);
width: 300px;
height: 300px;
border-radius: 20px;
}
</style>
</head>
<body>
<div id="box"></div>
</body>
<script>
let box = document.querySelector('#box');
let count = 0;
function noTouchMe() {
console.log('狗东西,别摸了!!!')
console.log(count++);
};
box.onmousemove = noTouchMe;
</script>
</html>
下面是,假如我在蓝色框里随便移动,我们可以看到旁边一直输出执行,结果输出了1000多次
那么要是我们把 noTouchMe 这个事件换为我们获取数据的接口,那么对于服务器来说,是不是非常快乐?是不是会给后端接口带来很大的压力。
所以防抖函数就是为了避免这个问题,在你确定前不会发送请求,比如输入框的话,是在你完全输入完闭后才进行接口的调用,而不是你每输入一个字就调用接口一次。
那么如何知道你是否已经输入完了呢?我们可以设置一个等待时间,要是这段时间里,你没有输入了,那么就判定你输入完毕了。针对我们在这里的例子就是要实现一个防抖函数,当我们在蓝色盒子中移动时,只有停下来,经过等待时间后还是没有移动了才执行事件,要是等待时间里移动了,那么重置等待时间。
//实现的目标一:事件在等待一定时间后才执行,要是还在该时间内,发生改变的话就重置等待时间
//防抖函数
function debounce(code , millisec) {
let timeout;
return function() {
clearTimeout(timeout);
// timeout为调用 setTimeout() 函数时所获得的返回值(调用次数),使用该返回标识符作为参数,可以取消该 setTimeout() 所设定的定时执行操作。
timeout = setTimeout(function() {
code();
} , millisec);
console.log('--------->' , timeout);
}
}
//执行的方法
let box = document.querySelector('#box');
let count = 0;
function getMessgee() {
console.log('狗东西,别摸了!!!')
console.log(count++);
};
//应用防抖函数
box.onmousemove = debounce(getMessgee, 300);
我们可以发现是不是很简单呀,我们给她添加一个防抖函数,函数里传入两个值,一个是方法,一个是等待的时间。在防抖函数里面, 定义一个变量来存储定时器返回的变量 timeout 来保证我们只要一改变了,就把定时器重置。
接下来我们还有的一个问题就是,我们这个执行函数(getMessgee)的 this 的指向是谁呢?其实我们要是使用上面实现的方法可以发现,是指向 window 的,但是我们不是应该指向我们当前的 dom 元素才是比较河里的吗? 下面我们就要进行一些改造。
没修改的结果:
实现修改的代码:
//实现的目标二:修正this的指向错误
//防抖函数
function debounce(code , millisec) {
let timeout;
return function() {
//改变执行函数内部的this的指向,这里我们容易知道 debounce 函数的this是指向当前元素的。
let context = this;
clearTimeout(timeout);
// timeout为调用 setTimeout() 函数时所获得的返回值(调用次数),使用该返回标识符作为参数,可以取消该 setTimeout() 所设定的定时执行操作。
timeout = setTimeout(function() {
code.apply(context);
} , millisec);
}
}
修改后的结果:
那么要是我们希望立即执行,不用去等待我们设置的时间后才执行呢?显然我们还得给我们的防抖函数再添加一个参数。用来判断是否立即执行。
改造如下:
//实现的目标三:修正this的指向错误
//防抖函数
function debounce(code, millisec, ifNow) {
let timeout;
return function() {
//改变执行函数内部的this的指向
let context = this;
let arges = arguments;
clearTimeout(timeout);
// timeout为调用 setTimeout() 函数时所获得的返回值(调用次数),使用该返回标识符作为参数,可以取消该 setTimeout() 所设定的定时执行操作。
//立即执行
if(ifNow){
let callNow = !timeout;
timeout = setTimeout(()=> {
timeout = null;
}, millisec);
if(callNow) code.apply(context, arges)
}else{
//不会立即执行
timeout = setTimeout(function() {
code.apply(context);
} , millisec);
}
}
}
这时候,我们只要一进入该函数就会马上执行了,不是等待后才执行。那么我们设置这个参数的和没有设置的区别在于前者是在我们设置的时间后执行,后者是马上执行,在我们设置的时间里,没有触发,才会再执行。可以说一个是头触发,一个是尾触发吧。到这里我们就基本完善好了我们的防抖函数了。
但我们再完善一下:
//实现的目标四:返回执行函数的值以及当我们需要取消防抖函数
//防抖函数
function debounce(code, millisec, ifNow) {
let timeout;
let result;
let debounced = function() {
//改变执行函数内部的this的指向
let context = this;
let arges = arguments;
clearTimeout(timeout);
// timeout为调用 setTimeout() 函数时所获得的返回值(调用次数),使用该返回标识符作为参数,可以取消该 setTimeout() 所设定的定时执行操作。
if(ifNow){
let callNow = !timeout;
timeout = setTimeout(()=> {
timeout = null;
}, millisec);
//立即执行
if(callNow) result = code.apply(context, arges)
}else{
//不会立即执行
timeout = setTimeout(function() {
result = code.apply(context, arges);
} , millisec);
}
return result
}
//把防抖函数变为对象,这样我们可以添加多一个取消的方法
debounced.cancel = function() {
clearTimeout(timeout)
//因为已经构成闭包,js的垃圾回收不了,导致内存泄露,我们这里要手动设置为null
timeout = null
}
return debounced
}
好啦,大功告成!
二、节流
使用场景:持续触发事件,会每隔一段时间,才执行执行一次
- DOM元素的拖拽功能
- 射击游戏
- 计算鼠标移动距离
和防抖相比,节流是持续触发事件,会每隔一段时间,才执行执行一次。比如在计算鼠标移动距离中。当我们计算鼠标的移动距离,要是没有使用节流函数,那么每移动1px,就会调用计算。可想而知,当我们轻轻滑动。就会调用触发无数次了,会给服务器带来极大的压力。但我们使用节流后,比如设置间隔时间,持续触发那么他就会每隔这个时间段触发一次。大大减少服务端压力。
那么我们来简单实现一下:
//实现目标:鼠标进入后,一直移动,但只会每隔两秒才会触发
//节流函数
function throttle(func, wait) {
let context, args;
//之前的时间戳
let old = 0;
return function() {
context = this;
args = arguments;
let now = new Date().valueOf();
if (now-old > wait) {
//立即执行
func.apply(context, args);
old = now
}
}
}
我们接着使用上面节流的小方块,期间我们一直在蓝色方块中移动,可以发现,它的输出,会每隔1000才会执行一次。那是我们上面的方法,我们根据时间戳,用前后两次的时间差来看是否大于我们设置的时间,大于的话就执行,同时赋值时间。
但是上面这种方式,我们可以看出,当我们的方法,实际一开始会触发,但是结束的时候不会触发。所以我们还有下面的方式,使用定时器来实现:
//解决上面的第一次会触发,最后一次不会触发的问题
function throttle2(func, wait) {
let context, args, timeout;
return function() {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args);
}, wait)
}
}
}
好啦,上面的实现了没头有尾,那么我们把他们合并一下,就有了完整的有头有尾了~~~
//节流函数
function throttle(func, wait) {
let context, args, timeout;
let old = 0;
let later = function() {
old = new Date().valueOf();
timeout = null;
func.apply(context, args);
}
return function() {
context = this;
args = arguments;
let now = new Date().valueOf();
if (now-old > wait) {
if(timeout) {
clearTimeout(timeout);
timeout = null
}
func.apply(context, args);
old = now
}else if (!timeout) {
timeout = setTimeout(later, wait)
}
}
}
这里实际就是把上面的两个方法合并起来的,但是我们哟留意的是他们过程中控制时间的变量,很容易导致出错。但是有时我们就想开始会触发或者结束不触发啥的~~(人真的是麻烦),那么我们能不能通过自定义来控制呢?
//节流函数
function throttle(func, wait, options) {
let context, args, timeout;
let old = 0;
if(!options) options = {};
let later = function() {
old = new Date().valueOf();
timeout = null;
func.apply(context, args);
}
return function() {
context = this;
args = arguments;
let now = new Date().valueOf();
if(options.leading === false && !old) {
old = now;
}
if (now-old > wait) {
if(timeout) {
clearTimeout(timeout);
timeout = null
}
func.apply(context, args);
old = now
}else if (!timeout && options.trailing != false) {
timeout = setTimeout(later, wait)
}
}
}
//调用的方法~~
box.onmousemove = throttle(getMessgee, 1000, {leading: false, trailing: false});
好啦,我们通过传入变量就能控制我们想要的结果啦~~~
三、解决请求有防抖但是需要马上响应或获取防抖状态的情况
描述问题: 当时的情况是,我们有一个输入框,根据输入框的输入内容,以及旁边的筛选功能,来确定下方区域显示的数据。同时我们有一个重置按键,点击会重置输入框的内容和筛选条件,显示当前查询数据为空的页面。出现的问题就是,当我们点击重置的时候,会短时间的出现缺省页,然后才显示全部数据。
为什么会出现这种情况呢?是因为我们的缺省页的显示是根据下方的区域有没有数据以及有没有筛选条件来判断的,有的话不会显示,都没有的话显示。
所以在点击重置按键后,数据因为防抖函数,会在一秒后才获取到,所以在这一秒的时间里,所有值都为空,就会导致缺省的的显示,并且在一秒,才消失。
那么如何解决这个问题呢?我们通过标记符的形式来记录!下面我们一步步道来。
解决原理: 我们面对的问题是,我们点击重置后会有一秒的缺省页显示,缺省页显示是因为获取数据的接口有一秒的防抖,还没有数据,导致判断条件为空。这里我们添加时间戳为第一个标志,给获取数据时的接口加上另外给标志,这时候应为防抖的一秒就会造成两者的不同,我们就能用来判断是否接口获取完数据了。就能排除防抖带来的影响。
- 第一步:我们创建一个方法来记录当前的时间戳,并且在我们点击重置时调用
//1.创建方法,并且在date里添加两个参数
data() {
return {
//.....
lastTimerRequest: null,
timerRequest: null
}
}
onSetChange() {
this.timerRequest = new Date().getTime();
}
//2.在点击重置的按键的上添加事件
- 在调用接口的前面加上
this.lastTimerRequest = this.timerRequest;
- 最后一步:在我们的计算属性中加上这个判断的参数。
const isUpload = this.timerRequest ? this.lastTimerRequest !== this.timerRequest : true;
return isUpload && ......
总结
好好学习~~~