防抖与节流总结(含原理与简实现)

260 阅读1分钟

一、概念与区别

  • 防抖(Debounce):

    • 频繁触发时只在最后一次触发后执行
    • 原理:每次触发都 clearTimeout 重置定时器
  • 节流(Throttle):

    • 固定时间间隔内最多执行一次
    • 原理:基于“比较时间”或“占位计时器”

二、最简实现

  • 防抖(尾部执行)
function debounce(fn, wait = 300) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}
  • 节流(时间戳版:leading,可能丢尾)
function throttle(fn, wait = 300) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      fn.apply(this, args);
    }
  };
}
  • 节流(定时器版:trailing,不立即)
function throttle(fn, wait = 300) {
  let timer = null;
  return function (...args) {
    if (timer) return;
    timer = setTimeout(() => {
      timer = null;
      fn.apply(this, args);
    }, wait);
  };
}

三、进阶(可选项)

  • 防抖(支持 leading/trailing)
function debounce(fn, wait = 300, { leading = false, trailing = true } = {}) {
  let timer = null, lastArgs, lastThis;
  const invoke = () => {
    timer = null;
    if (trailing && lastArgs) {
      fn.apply(lastThis, lastArgs);
      lastArgs = lastThis = null;
    }
  };
  const debounced = function (...args) {
    lastArgs = args; lastThis = this;
    const callNow = leading && !timer;
    clearTimeout(timer);
    timer = setTimeout(invoke, wait);
    if (callNow) { fn.apply(lastThis, lastArgs); lastArgs = lastThis = null; }
  };
  debounced.cancel = () => { clearTimeout(timer); timer = null; lastArgs = lastThis = null; };
  debounced.flush = () => { if (timer) { clearTimeout(timer); invoke(); } };
  return debounced;
}
  • 节流(同时支持 leading/trailing)
function throttle(fn, wait = 300, { leading = true, trailing = true } = {}) {
  let last = 0, timer = null, lastArgs, lastThis;
  const invoke = (time) => { last = time; fn.apply(lastThis, lastArgs); lastArgs = lastThis = null; };
  return function (...args) {
    const now = Date.now();
    if (!last && leading === false) last = now;
    const remaining = wait - (now - last);
    lastArgs = args; lastThis = this;
    if (remaining <= 0) {
      if (timer) { clearTimeout(timer); timer = null; }
      invoke(now);
    } else if (!timer && trailing !== false) {
      timer = setTimeout(() => { timer = null; invoke(leading === false ? Date.now() : last + wait); }, remaining);
    }
  };
}