真正搞懂防抖函数&节流函数

1,666 阅读4分钟

前言:

这两天看面试题,又遇到了防抖和节流,之前也是似懂非懂,于是到处看如何实现防抖和节流,可是很可惜,大部分只有代码没有实际场景和注释,于是自己弄懂后记录下来。

防抖函数

  • 定义:顾名思义,在频繁触发的前提下,让函数只在特定的时间内没有触发执行条件才执行一次代码。
  • 使用场景:频繁操作点赞和取消点赞,因此需要获取最后一次操作结果并发送给服务器。

自己模拟了一个使用场景,即点击提交按钮会触发ajax请求,但是如果用户频繁点击提交按钮,难道就要触发n次吗,显然不可取。

  1. 那么如何做呢?先写下初始代码。通过 console.log(1) 去模拟ajax 请求的场景。现在只要点击按钮就会打印 1 ,n 次点击就打印 n 次 1 。
<input type="text">
<button id="submit">提交</button>
let btn=document.getElementById('submit')
btn.addEventListener('click',submit)

function submit(){
	console.log(1)
}
  1. 有了场景,就要开始思考了,如何让 submit 函数防抖呢?首先给它包装一个 debounce 函数。但是这里有一个问题,在页面刷新时,会发现浏览器会自动打印 1 和 debounce。这是为什么呢?
btn.addEventListener('click',debounce(submit,1000))
function debounce(fn,delay){
	fn()
	console.log("debounce")
}
  1. 这是因为 addEventListener 中接收的第二个参数是立即执行函数,所以会导致 debounce 函数被执行。

  2. 那么就得想办法如何让 debounce 不执行。其实改写为回调函数即可。

function debounce(fn,delay){
  return function(){
    fn()
  }
}
  1. 接着开始思考,如何在点击按钮时,让函数延迟执行呢?
function debounce(fn,delay){
  return function(){
    setTimeout(() => {
      fn()
    }, delay);
  }
}
  1. 可是光延时哪儿行,该触发 n 次还是触发 n 次。就得想办法在第二次点击时让计时器重新计时。首先在回调函数外面定义一个 timer 用来接收计时器,由于 debounce 在页面加载时就会自动执行,所以 timer 初始值就是 null , 并且在后续点击按钮时,是直接触发的回调函数,不会去重新定义 timer 。
function debounce(fn,delay){
  let timer=null
  return function(){
    if(timer){
      clearTimeout(timer)
    }
    timer=setTimeout(() => {
      fn()
    }, delay);
  }
}
  1. 大致上,已经完成了防抖的基本操作,也能理解了,但是会有几个小问题。

  2. 关于 submit 函数的 this 指向问题。可以发现这里的 this 实际是 window 对象,为什么呢?这是因为 submit 是在计时器中被调用的。可是计时器中的 this 为什么指向按钮呢,这是因为计时器是箭头函数,箭头函数是不具备 this 的,this 取决于外层的函数,这里外层函数为回调函数,而这个回调函数指向 btn 。那么 fn() 是直接调用,所以它的 this 指向 window 。

function submit(){
  console.log(this);
}
function debounce(fn,delay){
  let timer=null
  return function(){
    if(timer){
      clearTimeout(timer)
    }
    timer=setTimeout(()=>{
      console.log(this);
      fn()
    }, delay);
  }
}
  1. 解决 submit 的 this 指向问题。这里只需要使用 apply 函数去改变 this 指向即可。
timer=setTimeout(()=>{
  console.log(this);
  fn.apply(this)
}, delay);
  1. 关于事件对象的获取。如果改写为如下就可以获取到 e 。
function submit(){
  console.log(e);
}
function debounce(fn,delay){
  return function(e){
    console.log(e);
  }
}
  1. 按照防抖函数的写法。debounce 可以获取到 e ,而 submit 是获取不到的。所以要思考如何传递参数。
function submit(){
  console.log(e);
}
function debounce(fn,delay){
  let timer=null
  return function(e){
    if(timer){
      clearTimeout(timer)
    }
    timer=setTimeout(()=>{
      console.log(e);
      fn.apply(this)
    }, delay);
  }
}
  1. arguments 可以用来传递参数,在回调函数中,arguments 就是传递给函数的所有参数集合。
function submit(e){
  console.log(e);
}
function debounce(fn,delay){
  let timer=null
  return function(){
    if(timer){
      clearTimeout(timer)
    }
    timer=setTimeout(()=>{
      fn.apply(this,arguments)
    }, delay);
  }
}

具体 html 代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>防抖</title>
</head>
<body>
  <input type="text">
  <button id="submit">提交</button>

  <script>
    let btn=document.getElementById('submit')
    btn.addEventListener('click',debounce(submit,1000))

    function submit(e){
      console.log(11111);
    }
    function debounce(fn,delay){
      let timer=null
      return function(){
        if(timer){
          clearTimeout(timer)
        }
        timer=setTimeout(()=>{
          fn.apply(this,arguments)
        }, delay);
      }
    }
  </script>
</body>
</html>

节流函数

这一块真的弄懂了防抖,其实也能很快写出来

  • 定义:频繁触发,但只在特定的时间内才执行一次代码
  • 场景:一些频繁触发的事件,但是规定在一段时间内只触发一次。和防抖函数有点类似,但是节流函数的第一次是执行的,然后在某个时间内,不管事件被触发多少次都不执行。

直接上 html 代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>防抖</title>
</head>
<body>

  <input type="text">
  <button id="submit">提交</button>

  <script>
    let btn=document.getElementById('submit')
    btn.addEventListener('click',throttle(submit,3000))

    function submit(e){
      console.log(e);
      console.log(this);
      console.log("执行");
    }
    function throttle(fn,delay){
      // 首先定义一个起始时间startTime
      let startTime=0
      return function (){
        // 定义一个当前的时间currentTime
        let currentTime=new Date().getTime()
        // 为了能让第一次事件就执行,就需要做一个判断,
        // 由于startTime为0,而currentTime是一个大大大大...大的数,
        // 所以肯定能够成功
        if(currentTime-startTime>delay){
          // 如果执行了,那么就调用fn
          fn.apply(this,arguments)
          // 同时,也要给startTime重新赋值,
          // 现在startTime就是第一次事件发生的时间戳
          // 等到下一次判断的时候,currentTime已经是第二次的时间戳了,
          // 所以通过第n次减去第(n-1)次就能得出两次事件触发的时间间隔了
          startTime=currentTime
        }else {
          console.log("正在节流,请稍等");
        }
      }
    }
  </script>
</body>
</html>