防抖与节流函数(立即执行版)

适用场景:

  • 防抖(Debounce):输入框搜索、窗口 resize 等“高频触发,停下来再放行”场景
  • 节流(Throttle):滚动监听、拖拽、按钮连续点击等“固定时间内最多执行一次”场景

下面给的是通用 TypeScript 版本,默认都是“立即执行(leading)”。

1. 防抖(立即执行)

特点:

  • 第一次触发立即执行
  • wait 时间内重复触发不再执行
  • 只有停止触发达到 wait 后,才允许下一次立即执行
type AnyFn = (...args: any[]) => any

type Debounced<T extends AnyFn> = ((...args: Parameters<T>) => ReturnType<T> | undefined) & {
  cancel: () => void
  flush: () => ReturnType<T> | undefined
}

export function debounceImmediate<T extends AnyFn>(
  fn: T,
  wait = 300,
): Debounced<T> {
  let timer: ReturnType<typeof setTimeout> | undefined
  let lastThis: unknown
  let lastArgs: Parameters<T> | undefined
  let lastResult: ReturnType<T> | undefined

  const debounced = function (this: unknown, ...args: Parameters<T>) {
    lastThis = this
    lastArgs = args

    const canRunNow = !timer
    if (timer) {
      clearTimeout(timer)
    }

    // 只负责“解锁”,不做尾调用
    timer = setTimeout(() => {
      timer = undefined
    }, wait)

    if (canRunNow) {
      lastResult = fn.apply(lastThis, lastArgs)
      return lastResult
    }

    return lastResult
  } as Debounced<T>

  debounced.cancel = () => {
    if (timer) {
      clearTimeout(timer)
      timer = undefined
    }
  }

  debounced.flush = () => {
    debounced.cancel()
    if (lastArgs) {
      lastResult = fn.apply(lastThis, lastArgs)
    }
    return lastResult
  }

  return debounced
}

2. 节流(立即执行)

特点:

  • 第一次触发立即执行
  • wait 时间内,后续触发最多在结尾补一次(可关)
  • 常用于滚动、拖拽这类持续触发场景
type AnyFn = (...args: any[]) => any

type Throttled<T extends AnyFn> = ((...args: Parameters<T>) => ReturnType<T> | undefined) & {
  cancel: () => void
  flush: () => ReturnType<T> | undefined
}

interface ThrottleOptions {
  trailing?: boolean
}

export function throttleImmediate<T extends AnyFn>(
  fn: T,
  wait = 300,
  options: ThrottleOptions = {},
): Throttled<T> {
  const { trailing = true } = options
  let timer: ReturnType<typeof setTimeout> | undefined
  let lastThis: unknown
  let lastArgs: Parameters<T> | undefined
  let lastResult: ReturnType<T> | undefined

  const run = () => {
    if (!lastArgs) return
    lastResult = fn.apply(lastThis, lastArgs)
    lastArgs = undefined
  }

  const throttled = function (this: unknown, ...args: Parameters<T>) {
    lastThis = this
    lastArgs = args

    // 立即执行
    if (!timer) {
      run()
      timer = setTimeout(() => {
        timer = undefined
        // 可选尾调用
        if (trailing && lastArgs) {
          run()
        }
      }, wait)
      return lastResult
    }

    // 冷却中:只更新最后一次参数,等待尾调用
    return lastResult
  } as Throttled<T>

  throttled.cancel = () => {
    if (timer) {
      clearTimeout(timer)
      timer = undefined
    }
    lastArgs = undefined
  }

  throttled.flush = () => {
    if (timer) {
      clearTimeout(timer)
      timer = undefined
    }
    if (lastArgs) {
      run()
    }
    return lastResult
  }

  return throttled
}

3. 使用示例

import { debounceImmediate, throttleImmediate } from '@/utils/function'

// 输入搜索:立即查一次,连续输入期间不再触发
const onSearch = debounceImmediate((keyword: string) => {
  console.log('search =>', keyword)
}, 400)

// 滚动监听:立即执行 + 冷却结束补最后一次
const onScroll = throttleImmediate(() => {
  console.log('scroll =>', Date.now())
}, 200)

window.addEventListener('scroll', onScroll)

// 页面销毁时记得移除监听并 cancel
// window.removeEventListener('scroll', onScroll)
// onScroll.cancel()

4. 常见坑

  • 防抖和节流都要注意组件卸载时 cancel(),避免内存泄漏或无效回调
  • wait 太小会让“优化”意义不大,太大又会影响交互手感
  • 搜索场景常用防抖,滚动/拖拽场景常用节流,不要反着用

5. 精简版(立即执行)

如果你只想要最短可用代码,用下面这版即可。

// 立即执行防抖:第一次立刻执行,wait 内重复触发不执行
export function debounceNow<T extends (...args: any[]) => void>(fn: T, wait = 300) {
  let timer: ReturnType<typeof setTimeout> | null = null

  return function (this: unknown, ...args: Parameters<T>) {
    if (!timer) {
      fn.apply(this, args)
    }
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      timer = null
    }, wait)
  }
}
// 立即执行节流:第一次立刻执行,wait 内只执行一次
export function throttleNow<T extends (...args: any[]) => void>(fn: T, wait = 300) {
  let canRun = true

  return function (this: unknown, ...args: Parameters<T>) {
    if (!canRun) return
    canRun = false
    fn.apply(this, args)
    setTimeout(() => {
      canRun = true
    }, wait)
  }
}

最小使用示例:

const onInput = debounceNow((v: string) => {
  console.log('search:', v)
}, 300)

const onScroll = throttleNow(() => {
  console.log('scroll')
}, 200)