防抖和节流
防抖处理
有时候高频的触发函数是我们不想见到的,于是我们可以使用防抖处理去解决这个问题。防抖,顾名思义,不管你怎么抖,我都只会触发高频连续函数的最后一次。
防抖简易版
我们通常会使用window的setTimeout方法处理防抖函数,代码如下:
function debounce(fn, await){
let timer = null;
return function(){
if(timer){
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(fn.arguments);
}, await);
}
}
这里我们使用闭包去存储setTimeout方法的变量,当下一次触发防抖函数的时候,会取消上一次的setTimeout方法。讲的简单点就是如果我们触发了一次该函数,在await时间内又触发了,上一次的setTimeout中的fn函数还没有执行呢,就被清除了,取而代之的是新的setTimeout方法,在await时间后去执行fn函数。
实验代码如下,在1s内高频点击只会触发最后一次。
<!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>
</head>
<body>
<button id="btn">点我一下</button>
</body>
<script>
function debounce(fn, await){
let timer = null;
return function(){
if(timer){
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(fn.arguments);
}, await);
}
}
function test(){
console.log("啊!!!,我被点击了");
}
document.querySelector('#btn').addEventListener('click', debounce(test,1000));
</script>
</html>
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>Document</title>
</head>
<body>
<button id="btn">点我一下</button>
</body>
<script>
function debounce(fn, await){
let timer = null;
return function(){
if(timer){
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(fn.arguments);
}, await);
}
}
function test(event){
console.log("我的this指向的是" + this);
console.log("我的event是" + event);
}
document.querySelector('#btn').addEventListener('click', debounce(test,1000));
</script>
</html>
当我们点击按钮的时候,结果并不是我们所希望的结果。
test函数里的this竟然是指向window,event没有了。我们这可是希望绑定给button的啊,无语了。
我们来看下一个例子就明白了,大家来猜猜结果是指向小黄还是小白。
<!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>
</head>
<body>
</body>
<script>
const obj1 = {
name:"小黄",
sayHello(fn){
fn();
}
}
const obj2 = {
name:"小白",
fn(){
console.log(this);
}
}
obj1.sayHello(obj2.fn);
</script>
</html>
结果发现都不是,哈哈,指向的还是是window。
所以我们大概能想到,这种将函数作为参数传递给另一个函数,并在另一个函数中使用会使this丢失。我们需要在防抖处理函数中纠正它的this指向。代码如下:
function debounce(fn, await){
let timer = null;
return function(){
if(timer){
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arguments);//就只有这里发生了改变。
}, await);
}
}
//apply方法改变函数的this指向,具体的函数规则可以自行百度,我们只需要知道这个这个方法将fn的this强行改成了debounce作用域的this。
结果就正常了:
加上立即执行功能
我们可以在上面的代码进行改变,以上的处理是在await时间后再执行fn函数,我们希望可以触发debounce函数时立即执行fn函数,在高频触发debounce函数只执行第一次,代码如下:
function debounce(fn, await, immediate){//fn定义的函数,await等待的时间,immediate是是立即执行
let timer = null;
return function(){
if(timer){
clearTimeout(timer);
}
if(immediate){
if(timer === null){
fn.apply(this, arguments);
}
timer = setTimeout(()=>{
timer = null;
},await);
}else{
timer = setTimeout(() => {
fn.apply(this, arguements);
}, await);
console.log();
}
}
}
当immediate取true的时候证明开启立即执行,这里的timer起到了存储上一次等待时间是否过完了,如果没有过完,timer就不是null,也就不会执行fn函数。只有上一次的等待时间过完了,才会执行fn函数。思路和上面的差不多,我就不给大家展示测试的结果了。
节流处理
和防抖处理一样,都是解决高频触发函数的处理方式,不过它的思路不是取多次高频触发的最后一次,而是只有在确定时间间隔过了才会触发。举个例子:我们在5秒内触发了20次函数,间隔为2s,那么经过节流处理函数只会触发两次,在2s和4s的时候。不说了,直接show code:
function jieliu(fn, await){
let timer = null;
let flag = true;
return function(){
if(flag){
timer = setTimeout(() => {
fn.apply(this, arguments);
flag = true;
}, await);
}
flag = false;
}
}
这里的flag用来存储上一次上一次setTimeout是否执行完,注意setTimeout是异步执行的,所以理论上flag一直都是false,只有上一次的setTimeout执行完,下一次的setTimeout才能被触发。
实验代码如下:
<!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>
</head>
<body>
<button id="btn">点我一下</button>
</body>
<script>
function jieliu(fn, await){
let timer = null;
let flag = true;
return function(){
if(flag){
timer = setTimeout(() => {
fn.apply(this, arguments);
flag = true;
}, await);
}
flag = false;
}
}
function test(){
console.log("我被触发了");
}
document.querySelector('#btn').addEventListener('click', jieliu(test,1000));
</script>
</html>
节流增强版
当然也会发生this丢失的问题,我们在上面的基础上进行改进,和防抖处理改进的思路一样。
function jieliu(fn, await, immediate){
let timer = null;
let mark = true;
return function(){
if(mark){
if(immediate){
fn.call(this, ...arguments);
timer = setTimeout(()=>{
mark = true;
},await)
}else{
timer = setTimeout(()=>{
fn.call(this, ...arguments);
mark = true;
},await)
}
}
mark = 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>Document</title>
</head>
<body>
<button id="btn">点我一下</button>
</body>
<script>
function jieliu(fn, await, immediate){
let timer = null;
let mark = true;
return function(){
if(mark){
if(immediate){
fn.call(this, ...arguments);
timer = setTimeout(()=>{
mark = true;
},await)
}else{
timer = setTimeout(()=>{
fn.call(this, ...arguments);
mark = true;
},await)
}
}
mark = false;
}
}
function test(event){
console.log("我被触发了");
console.log("我的this指向的是" + this);
console.log("我的event是" + event);
}
document.querySelector('#btn').addEventListener('click', jieliu(test,1000, true));
</script>
</html>
注意事项
我们在解决this指向的问题时,在setTimeout的回调函数中用的是箭头函数,箭头函数的this指向的是外部环境的this。如果你用普通函数,指向的就是window了,因为setTimeout是window的方法,需要用变量将外部环境的this保存下来。