Skip to content

防抖和节流

debounce - 防抖 throttle - 节流

防抖和节流很相似,但是不同,本质上是控制函数在一段时间内执行多少次的技术。

相同点是避免函数在单位时间连续执行,从而提高性能。 两者的本质区别:throttle 可以保证函数在单位时间 T 内至少执行一次,而 debounce 不会保证。

防抖

延迟单位时间后执行回调,如果函数在单位时间里再次触发,则重新计时。

使用场景:

  • 防止按钮多次点击:向后台提交表单时,只执行最后一次点击的逻辑。
  • 后端搜索场景:在输入框停止输入后,发起 http 请求。

基础防抖

js
function debounce(fn, delay) {
  let timer; // 暂存 timeout 定时器函数
  const debounced = (...args) => {
    timer && clearTimeout(timer) // 每次调用 debounced 函数时,取消暂存的定时器,避免多次调用 setTimeout 回调
    // 重置定时器,在停止调用 debounced 函数至少 delay 毫秒后调用 fn
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
  return debounced
}

立刻执行防抖

js
function debounceLeading(fn, delay) {
  let timer;
  const debounced = (...args) => {
    // 判断是否可调用 fn
    // 1. 首次点击调用
    // 2. 距离上次调用 delay 毫秒后可再次调用
    if (!timer) {
      fn.apply(this, args)
    }
    timer && clearTimeout(timer) // 连续调用 debounced 函数时取消定时器
    // 重置定时器
    timer = setTimeout(() => {
      timer = undefined // 在上次调用 delay 毫秒后重置 timer
    }, delay)
  }
  return debounced
}

立刻执行且执行最后一次调用防抖

js
function debounceLeadingTrailing(fn, delay) {
  let timer;
  const debounced = (...args) => {
    // 判断是否可调用 fn
    // 1. 首次点击调用
    // 2. 距离上次调用 delay 毫秒后可再次调用
    if (!timer) {
      fn.apply(this, args)
    }
    timer && clearTimeout(timer) // 连续调用 debounced 函数时取消定时器
    // 重置定时器
    timer = setTimeout(() => {
      timer = undefined // 在上次调用 debounced 函数 delay 毫秒后重置 timer
      fn.apply(this, args) // 上次调用 debounced 函数 delay 毫秒后调用 fn
    }, delay)
  }
  return debounced
}

节流

在单位时间内,只执行一次回调。如果单位时间连续触发多次函数,有一次会生效。

使用场景:

  • 拖拽场景:固定时间只执行一次,防止超高频连续的位置变化引起的卡顿。
  • 缩放场景:监听浏览器 resize。
  • 动画场景:避免短时间内多次触发动画。

基础节流

js
function throttle(fn, delay) {
  let delayOk = true // delay 间隔是否重置
  const debounced = (...args) => {
    if (delayOk) { // 在 delay 期间禁止执行
      fn.apply(this, args)
      delayOk = false // 调用 fn 后,开始计算 delay 间隔
      setTimeout(() => {
        delayOk = true // delay 完成,重置间隔
      }, delay)
    }
  }
  return debounced
}

执行最后一次调用节流

js
function throttleTrailing(fn, delay) {
  let timer
  let last = 0 // fn 上次调用时间
  const debounced = (...args) => {
    // 调用 fn,重置上次调用 fn 时间
    function execFn() {
      fn.apply(this, args)
      last = Date.now()
    }
    timer && clearTimeout(timer) // debounced 调用前清空定时器
    const diff = Date.now() - last // 本次调用 debounced 距上次调用 fn 的间隔
    if (diff > delay) { // 如果调用间隔超过 delay
      execFn() // 调用 fn, 重置上次调用 fn 时间
    } else {
      // 如果间隔小于等于 delay,重置定时器和定时器延迟时间
      // 因为要保证本次调用是在距上次 delay 毫秒后调用 , 则本次延时时间为 delay - diff
      timer = setTimeout(() => {
        execFn()
      }, delay - diff)
    }
  }
  return debounced
}