JavaScript的防抖、节流详解--每天进步一点点

168 阅读3分钟

前言

  1. js中的一些事件如浏览器的resize、scroll,鼠标的mousemove、mouseover,input输入框的keypress等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制。

  2. 我们可以采用 debounce(防抖)throttle(节流) 的方式来减少调用频率,同时又不影响实际效果。

防抖

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

  2. 效果:如果短时间内大量触发同一事件,只会执行一次函数。

  3. 个人理解:函数防抖就是王者荣耀的回城,按了回城需要读条完成才能回城,读条没完再按回城就会重新读条。

  4. 常见应用场景:

    • 搜索框搜索输入。只需用户最后一次输入完,再发送请求。
    • 手机号、邮箱验证输入检测。
    • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
  5. 实现:

    1. 根据防抖的的定义,使用 setTimeout 实现最简单的代码:

      
      /*
      * fn [function] 需要防抖的函数
      * delay [number] 毫秒,防抖期限值
      */
      
      let timer // 维护同一个timer
      
      function debounce (fn, delay) {
        clearTimeout(timer)
      
        timer = setTimeout(() => {
          fn()
        }, delay)
      }
      
      // 使用
      document.onmousemove = () => {
        debounce(fn, 1000)
      }
      
      
    2. 上面的方法有三个问题:

      • 一般防抖函数调用:fn = debounce(fn, 800)

      • timer只能在setTimeout的父级作用域中,这样才是同一个timer

      • fn方法有传参的情况

    3. 所以使用闭包和 apply 来解决:

      
      /*
      * fn [function] 需要防抖的函数
      * delay [number] 毫秒,防抖期限值
      */
      
      function debounce (fn, delay) {
        let timer
      
        return (...args) => {
          if (timer) {
            clearTimeout(timer)
          }
          timer = setTimeout(() => {
            fn.apply(this, args)
          }, delay)
        }
      }
      
      

节流

  1. 定义:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

  2. 效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。

  3. 个人理解:函数节流就是王者荣耀的普攻,就算狂点普攻按钮,也只会在规定的攻速内发动一个普通攻击。

  4. 常见应用场景:

    • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断。
    • 鼠标不断点击触发,mousedown(单位时间内只触发一次)。
  5. 实现:

    1. 定时器 + 时间戳

    2. 定时器:同防抖实现思路,注意代码有点细微的差别。

      
      /*
      * fn [function] 需要节流的函数
      * delay [number] 毫秒,节流期限值
      */
      
      function throttle (fn, delay) {
        let timer
      
        return (...args) => {
          if (timer) {
            return
          }
          timer = setTimeout(() => {
            fn.apply(this, args)
            timer = null
          }, delay)
        }
      }
      
      
    3. 时间戳:通过比对上一次执行时间与本次执行时间的时间差与间隔时间的大小关系,来判断是否执行函数。若时间差大于间隔时间,则立刻执行一次函数。并更新上一次执行时间。

      
      /*
      * fn [function] 需要节流的函数
      * delay [number] 毫秒,节流期限值
      */
      
      function throttle (fn, delay) {
        let preTime = Date.now()
      
        return (...args) => {
          const nowTime = Date.now()
      
          // 如果两次时间间隔超过了指定时间,则执行函数。
          if (nowTime - preTime >= delay) {
            preTime = Date.now()
            return fn.apply(this, args)
          }
        }
      }
      
      

异同

  1. 相同点:函数防抖和函数节流都是防止某一时间频繁触发。

  2. 不同点:

    • 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数。- 函数防抖只是在最后一次事件后才触发一次函数。

参考文献

  1. 7分钟理解JS的节流、防抖及使用场景
  2. 「中高级前端面试」手写代码合集
  3. 彻底弄懂函数防抖和函数节流
  4. 浅谈 JS 防抖和节流
  5. JS的防抖与节流