码农之家

不会写防抖节流?还不快进来学!

防抖、节流

开发中经常会有一些持续触发的事件,比如scroll、resize、mousemove、input等。频繁的执行回调,不仅对性能有很大影响,甚至会有相应跟不上,造成页面卡死等现象。

针对这种问题有两种解决方案,防抖和节流。

防抖

事件触发后的time时间内只执行一次。原理是维护一个延时器,规定在time时间后执行函数,如果在time时间内再次触发,则取消之前的延时器重新设置。所以回调只在最后执行一次。

1.第一种方法

    function debounce(func, time = 0) {
      if (typeof func !== 'function') {
        throw new TypeError('Expect a function')
      }

      let timer

      return function (e) {
        if (timer) {
          clearTimeout(timer)
        }

        timer = setTimeout(() => {
          func.call(this, e)
          clearTimeout(timer)
        }, time)
      }
    }

2.第二种方法

    const debounce = (func, wait = 0) => {
      let timeout = null
      let args
      function debounced(...arg) {
        args = arg
        if(timeout) {
          clearTimeout(timeout)
          timeout = null
        }
        // 以Promise的形式返回函数执行结果
        return new Promise((res, rej) => {
          timeout = setTimeout(async () => {
            try {
              const result = await func.apply(this, args)
              res(result)
            } catch(e) {
              rej(e)
            }
          }, wait)
        })
      }
      // 允许取消
      function cancel() {
        clearTimeout(timeout)
        timeout = null
      }
      // 允许立即执行
      function flush() {
        cancel()
        return func.apply(this, args)
      }
      debounced.cancel = cancel
      debounced.flush = flush
      return debounced
    }

节流

在事件触发过程中,每隔wait执行一次回调。

可选参数三 trailing,事件第一次触发是否执行一次回调。默认true,即第一次触发先执行一次。

  1. 第一种方法
    function throttle(func, wait = 0, trailing = true) {
      if (typeof func !== 'function') {
        throw new TypeError('Expected a function')
      }

      let timer
      let start = +new Date

      return function (e) {
        const now = +new Date
        const distance = start + wait - now

        if (timer) {
          clearTimeout(timer)
        }
        if (now - start >= wait || trailing) { // 每个wait时间执行一次或第一次触发就执行一次
          func.apply(this, rest)
          start = now
          trailing = false
        } else {
          // 事件结束wait时间后再执行一次
          timer = setTimeout(() => {
            func.call(this, e)
          }, distance )
        }
      }
    }

2. 第二种方法

    function throttlew(fn, wait) {
        let start = 0

        return function(e) {
            const now = Date.now()
            if (now - start > wait) {
                fn.call(this, e)
                start = now
            }
     }    

3.第三种方法

    const throttle = (func, wait = 0, execFirstCall) => {
        let timeout = null
        let args
        let firstCallTimestamp

        function throttled(...arg) {
          if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
          if (!execFirstCall || !args) {
            // console.log('set args:', arg)
            args = arg
          }
          if (timeout) {
            clearTimeout(timeout)
            timeout = null
          }
          // 以Promise的形式返回函数执行结果
          return new Promise(async (res, rej) => {
            if (new Date().getTime() - firstCallTimestamp >= wait) {
              try {
                const result = await func.apply(this, args)
                res(result)
              } catch (e) {
                rej(e)
              } finally {
                cancel()
              }
            } else {
              timeout = setTimeout(async () => {
                try {
                  const result = await func.apply(this, args)
                  res(result)
                } catch (e) {
                  rej(e)
                } finally {
                  cancel()
                }
              }, firstCallTimestamp + wait - new Date().getTime())
            }
          })
        }
        // 允许取消
        function cancel() {
          clearTimeout(timeout)
          args = null
          timeout = null
          firstCallTimestamp = null  // 这里很关键。每次执行完成后都要初始化
        }
        // 允许立即执行
        function flush() {
          cancel()
          return func.apply(this, args)
        }
        throttled.cancel = cancel
        throttled.flush = flush
        return throttled
      }

区别:

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

场景:

防抖:比如监听页面滚动,滚动结束并且到达一定距离时显示返回顶部按钮,适合使用防抖。

节流:比如在页面的无限加载场景下,需要用户在滚动页面时过程中,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。此场景适合用节流来实现。

总结

防抖:事件触发后的time时间内只执行一次。
节流:在事件触发过程中,每隔wait执行一次回调。

原文链接:https://juejin.cn/post/7221820151397695525 作者:整天想死的鱼