防抖与节流

先理解原理和示例,再练习完整口述与追问。

触发频率、this 与参数、首尾调用、取消与刷新。

防抖的核心状态是最近参数、接收者与等待计时器;每次触发重置尾调用。节流则定义时间窗口或下一次允许执行时刻,并决定是否补最后一次。leading/trailing 不同组合会改变单次调用和持续调用的行为,需要先讲契约再实现。

按 0、50、100、500ms 四次触发手算输出:尾部防抖只在安静窗口后执行最近参数,首尾节流可能先执行第一次并在窗口尾补最近一次。再验证 cancel 后不会执行,flush 是否立即处理待执行任务,执行前清理参数能否避免长时间保留大对象。

时间精度不保证严格固定;异步回调返回值和并发是否由工具管理需说明。原题代码给出了具体实现,不应只背“一个等停一个限频”。

防抖不断推迟执行,直到连续操作安静下来;节流在持续触发期间限制执行频率。防抖适合输入搜索,节流适合滚动处理。下面防抖默认只保留尾调用,可配置首调用;节流默认首调用加尾调用。它们都保留调用者的 this 和最后参数,并支持 cancel。它们只控制回调触发,不能解决已发请求的响应乱序。

js
function debounce(fn, wait, { leading = false, trailing = true } = {}) {
  if (typeof fn !== 'function') throw new TypeError('fn 必须是函数')
  if (!Number.isFinite(wait) || wait < 0) throw new RangeError('wait 不合法')
  let timer = null
  let args
  let context
  let result

  function invoke() {
    const currentArgs = args
    const currentContext = context
    args = context = undefined
    result = Reflect.apply(fn, currentContext, currentArgs)
    return result
  }

  function wrapped(...nextArgs) {
    const first = timer === null
    args = nextArgs
    context = this
    if (timer !== null) clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
      if (trailing && args) invoke()
      else args = context = undefined
    }, wait)
    if (leading && first) invoke()
    return result
  }

  wrapped.cancel = () => {
    if (timer !== null) clearTimeout(timer)
    timer = null
    args = context = undefined
  }
  wrapped.flush = () => {
    if (timer !== null) clearTimeout(timer)
    timer = null
    if (trailing && args) return invoke()
    args = context = undefined
    return result
  }
  return wrapped
}

function throttle(fn, wait, { leading = true, trailing = true } = {}) {
  if (typeof fn !== 'function') throw new TypeError('fn 必须是函数')
  if (!Number.isFinite(wait) || wait < 0) throw new RangeError('wait 不合法')
  let timer = null
  let lastTime = null
  let args
  let context
  let result

  function invoke(time) {
    lastTime = time
    const currentArgs = args
    const currentContext = context
    args = context = undefined
    result = Reflect.apply(fn, currentContext, currentArgs)
    return result
  }

  function later() {
    timer = null
    if (trailing && args) {
      invoke(Date.now())
      if (!leading) lastTime = null
    } else {
      args = context = undefined
      if (!leading) lastTime = null
    }
  }

  function wrapped(...nextArgs) {
    const now = Date.now()
    if (!leading && !trailing) return result
    args = nextArgs
    context = this
    if (lastTime === null) {
      if (leading) return invoke(now)
      lastTime = now
    }
    const remaining = wait - (now - lastTime)
    if (remaining <= 0 || remaining > wait) {
      if (timer !== null) clearTimeout(timer)
      timer = null
      return invoke(now)
    }
    if (trailing && timer === null) timer = setTimeout(later, remaining)
    if (!trailing) args = context = undefined
    return result
  }

  wrapped.cancel = () => {
    if (timer !== null) clearTimeout(timer)
    timer = null
    lastTime = null
    args = context = undefined
  }
  return wrapped
}

示例:debounce(search, 300) 等最后一次输入后执行;throttle(onScroll, 100) 限制频率。防抖的 leading:true,trailing:true 在一个窗口内仅调用一次时不会重复尾调用;有后续触发才执行尾调用。防抖返回最近实际执行结果,不是“未来结果 Promise”。时间与额外状态均为每次调用 O(1)。