Skip to content
索引

防抖与节流

本质上是优化高频率执行代码的一种手段

如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用 防抖(debounce)节流(throttle) 的方式来减少调用频率

定义

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应

假设电梯有两种运行策略 debouncethrottle,超时设定为15秒,不考虑容量限制

电梯第一个人进来后,15秒后准时运送一次,这是节流

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖

实现防抖

js
function debounce(func, wait) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  }
}
function debounce(func, wait) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  }
}

该函数接受两个参数:func 是要防抖的函数,wait 是等待时间(毫秒)。

使用方法:

js
// 定义一个函数
function sayHi() {
  console.log('hi');
}
// 将函数包装成防抖函数
const debouncedSayHi = debounce(sayHi, 1000);
// 调用防抖函数
debouncedSayHi();
debouncedSayHi();
debouncedSayHi();
// 定义一个函数
function sayHi() {
  console.log('hi');
}
// 将函数包装成防抖函数
const debouncedSayHi = debounce(sayHi, 1000);
// 调用防抖函数
debouncedSayHi();
debouncedSayHi();
debouncedSayHi();

在这个例子中,防抖函数 debouncedSayHi 会等待 1 秒钟后才执行 sayHi 函数,如果在 1 秒内调用多次 debouncedSayHi 函数,只会执行最后一次调用。

js
 function debounce(func, wait) {
        let timeout
        return () =>{
          clearTimeout(timeout)
          timeout = setTimeout(() => {
            func()
          }, wait)
        }
      }
      window.onresize = debounce(() => {
        console.log(123)
      }, 1000)
 function debounce(func, wait) {
        let timeout
        return () =>{
          clearTimeout(timeout)
          timeout = setTimeout(() => {
            func()
          }, wait)
        }
      }
      window.onresize = debounce(() => {
        console.log(123)
      }, 1000)
js
   function debounce(func, wait) {
        let timeout
        return function () {
          let context = this // 保存this指向
          let args = arguments // 拿到event对象
          clearTimeout(timeout)
          timeout = setTimeout(function () {
            func.apply(context, args)
          }, wait)
        }
      }
      window.onresize = debounce(() => {
        console.log(123)
      }, 1000)
   function debounce(func, wait) {
        let timeout
        return function () {
          let context = this // 保存this指向
          let args = arguments // 拿到event对象
          clearTimeout(timeout)
          timeout = setTimeout(function () {
            func.apply(context, args)
          }, wait)
        }
      }
      window.onresize = debounce(() => {
        console.log(123)
      }, 1000)
js


实现节流

在规定的间隔时间内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发

js
  <script>
      /**
       * 防抖节流
       * @param {*} action 回调
       * @param {*} delay 等待的时间
       * @param {*} context this指针
       * @param {Boolean} iselapsed 是否等待上一次
       * @returns {Function}
       */
      function throttle(action, delay, context, iselapsed) {
        let timeout = null
        let lastRun = 0
        return function () {
          if (timeout) {
            if (iselapsed) {
              return
            } else {
              clearTimeout(timeout)
              timeout = null
            }
          }
          let elapsed = Date.now() - lastRun
          let args = arguments
          if (iselapsed && elapsed >= delay) {
            runCallback()
          } else {
            timeout = setTimeout(runCallback, delay)
          }
          /**
           * 执行回调
           */
          function runCallback() {
            lastRun = Date.now()
            timeout = false
            action.apply(context, args)
          }
        }
      }
    </script>
  <script>
      /**
       * 防抖节流
       * @param {*} action 回调
       * @param {*} delay 等待的时间
       * @param {*} context this指针
       * @param {Boolean} iselapsed 是否等待上一次
       * @returns {Function}
       */
      function throttle(action, delay, context, iselapsed) {
        let timeout = null
        let lastRun = 0
        return function () {
          if (timeout) {
            if (iselapsed) {
              return
            } else {
              clearTimeout(timeout)
              timeout = null
            }
          }
          let elapsed = Date.now() - lastRun
          let args = arguments
          if (iselapsed && elapsed >= delay) {
            runCallback()
          } else {
            timeout = setTimeout(runCallback, delay)
          }
          /**
           * 执行回调
           */
          function runCallback() {
            lastRun = Date.now()
            timeout = false
            action.apply(context, args)
          }
        }
      }
    </script>
js
  <script>
      /**
       * 防抖节流
       * @param {*} action 回调
       * @param {*} delay 等待的时间
       * @param {*} context this指针
       * @param {Boolean} iselapsed 是否等待上一次
       * @returns {Function}
       */
      function throttle(action, delay, context, iselapsed) {
        let timeout = null
        let lastRun = 0
        return function () {
          if (timeout) {
            if (iselapsed) {
              return
            } else {
              clearTimeout(timeout)
              timeout = null
            }
          }
          let elapsed = Date.now() - lastRun
          let args = arguments
          if (iselapsed && elapsed >= delay) {
            runCallback()
          } else {
            timeout = setTimeout(runCallback, delay)
          }
          /**
           * 执行回调
           */
          function runCallback() {
            lastRun = Date.now()
            timeout = false
            action.apply(context, args)
          }
        }
      }
    </script>
  <script>
      /**
       * 防抖节流
       * @param {*} action 回调
       * @param {*} delay 等待的时间
       * @param {*} context this指针
       * @param {Boolean} iselapsed 是否等待上一次
       * @returns {Function}
       */
      function throttle(action, delay, context, iselapsed) {
        let timeout = null
        let lastRun = 0
        return function () {
          if (timeout) {
            if (iselapsed) {
              return
            } else {
              clearTimeout(timeout)
              timeout = null
            }
          }
          let elapsed = Date.now() - lastRun
          let args = arguments
          if (iselapsed && elapsed >= delay) {
            runCallback()
          } else {
            timeout = setTimeout(runCallback, delay)
          }
          /**
           * 执行回调
           */
          function runCallback() {
            lastRun = Date.now()
            timeout = false
            action.apply(context, args)
          }
        }
      }
    </script>

区别

相同点:

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeoutsetTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次

只触发最后一次事件

js
let iTime = ""
const scollThrottle = () => {
  clearTimeout(iTime)
  iTime = setTimeout(scroll, 100)
}
let iTime = ""
const scollThrottle = () => {
  clearTimeout(iTime)
  iTime = setTimeout(scroll, 100)
}

函数只执行一次

方法一,清空函数

js
let func = function () {
  console.log("正常调用")
  func = function () {}
}
func() 
let func = function () {
  console.log("正常调用")
  func = function () {}
}
func() 

方法二,定义全局boolen变量flag进行判断

js
let flag = true
function once() {
  if (flag) {
    console.log("正常调用")
    flag = false
  } else {
    return
  }
}
once()
let flag = true
function once() {
  if (flag) {
    console.log("正常调用")
    flag = false
  } else {
    return
  }
}
once()

应用场景

只需用户最后一次输入完,再发送请求,因此只需触发一次回调,使用防抖:

  • 搜索框搜索输入。
  • 手机号、邮箱验证输入检测
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染

在间隔一段时间执行一次回调,使用节流:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

Released under the MIT License.