一文搞懂防抖和节流的实现以及区别

164 阅读4分钟

防抖

什么是防抖

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

举个例子,一个按钮点击后将会发送网络请求,但是如果用户连续5次,会发送5次网络请求吗?答案是不会的,因为在每次请求后,都会等几秒再执行回调,如果再次被点击,将会重新计时,这就是函数防抖

使用场景

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 搜索框联想场景:防止联想发送请求,只发送最后一次输入

实现防抖

现在我们有一个按钮,在点击后它将输出hello

const btn = document.querySelector("button");
function say() {
  console.log("hello");
}
btn.addEventListener("click", say);

接下来我们来一步步实现函数防抖

创建防抖函数

const btn = document.querySelector("button");
function say() {
  console.log("hello");
}
// 防抖函数
function debounce(fn){
  fn();
}
btn.addEventListener("click", debounce(say));

如果你实现这段代码的话你会发现,在没有点击的时候,say()函数就已经执行了,这是因为定义监听函数的时候就已经执行了函数,因此我们需要使用高阶函数,也就是在函数里面返回函数,这也是防抖的第一个难点

const btn = document.querySelector("button");
function say() {
  console.log("hello");
}
// 防抖函数
function debounce(fn){
  return function(){
    fn();
  }
}
btn.addEventListener("click", debounce(say));

添加延时

const btn = document.querySelector("button");
function say() {
  console.log("hello");
}
// 防抖函数
function debounce(fn,delay){
  return function(){
    setTimeout(()=>{
      fn();
    },delay)
  }
}
btn.addEventListener("click", debounce(say,1000));

在上述代码中,我们加入了定时器,并且可以让使用者自定义延时时间,接着我们需要实现在每次点击前清除延时的功能

清除延时

const btn = document.querySelector("button");
function say() {
  console.log("hello");
}
// 防抖函数
function debounce(fn,delay){
  return function(){
    // 未声明就使用,报错
    clearTimeout(timer);
    let timer = setTimeout(()=>{
      fn();
    },delay)
  }
}
btn.addEventListener("click", debounce(say,1000));

很明显上述代码存在问题,timer在未声明就使用,因此我们需要在前面提前声明timer变量

function debounce(fn,delay){
  return function(){
    let timer;
    clearTimeout(timer);
    let timer = setTimeout(()=>{
      fn();
    },delay)
  }
}
btn.addEventListener("click", debounce(say,1000));

实现上述代码后,我们执行程序的话可以发现并未达到我们想要的效果,这是因为每次执行的函数内部都是独立的timer变量,因此我们需要通过闭包延长作用域链,这是防抖的第二个难点

function debounce(fn,delay){
  let timer;
  return function(){
    clearTimeout(timer);
    let timer = setTimeout(()=>{
      fn();
    },delay)
  }
}
btn.addEventListener("click", debounce(say,1000));

执行程序后发现,我们已经实现了防抖功能了,但是还没完,让我们回到say()方法里输出一下this

function say() {
  console.log("hello");
  console.log(this);
}

function debounce(fn,delay){
  let timer;
  return function(){
    clearTimeout(timer);
    let timer = setTimeout(()=>{
      fn();
    },delay)
  }
}
//btn.addEventListener("click",say); // this指向button
btn.addEventListener("click", debounce(say,1000)); // this指向Window

很明显,在按钮绑定say方法的时候,使用隐式绑定,this指向button,而在按钮绑定防抖函数的时候,出现了回调函数中隐式绑定丢失的问题,因此this指向Window。我们可以通过使用apply、call、bind方法实现硬绑定来解决这个问题

function debounce(fn,delay){
  let timer;
  return function(){
    let context = this;
    clearTimeout(timer);
    let timer = setTimeout(()=>{
      fn.apply(context)
    },delay)
  }
}
btn.addEventListener("click", debounce(say,1000)); // this指向button

防抖函数就正式完成了

节流

什么是节流

节流的核心是:触发事件,执行任务并设置时间间隔,如果时间间隔内有触发行为就取消任务,如果时间间隔后有触发行为,就再次执行任务并设置时间间隔

定时器版本

在持续触发事件的过程中,函数不会立即执行,并且会等待delay秒执行一次

function throttle(fn,delay){
  let timeout;
  return function(){
    let context = this;
    let args = arguments;
    if(!timeout){
      timeout = setTimeout(function(){
        timeout = null;
        fn.apply(context,args);
      },delay)
    }
  }
}

时间戳版本

在持续触发事件的过程中,函数会立即执行,并且会每delay秒执行一次

function throttle(fn,delay){
  let pre = 0;
  return function(){
    let context = this;
    let args = arguments;
    let now = +new Date();
    if(now - pre > delay){
      fn.apply(context,args);
      pre = now;
    }
  }
}

防抖与节流的区别

如果事件触发频繁,防抖中的计时会不断重置,从而会不断延迟了函数的执行,而节流中的计时器不会因为事件的触发而重置,仅仅是取消事件的触发