Skip to content

四、节流

快速开始

基础时间戳版

利用时间戳来判断时间差。这种方式的特点是立即执行,也就是在事件触发的一瞬间就会执行一次,然后在设定的时间内不再执行。

javascript
function throttle(fn, delay) {
  let previous = 0;

  return function(...args) {
    const now = Date.now();
    
    // 如果当前时间减去上次执行时间大于设定的延迟
    if (now - previous > delay) {
      fn.apply(this, args);
      previous = now; // 更新上次执行时间
    }
  };
}

// 使用示例:监听滚动,每 100ms 执行一次
window.addEventListener('scroll', throttle(() => {
  console.log('滚动中...', Date.now());
}, 100));

定时器版

使用 setTimeout 来实现。这种方式的特点是延迟执行,即事件触发后不会立即执行,而是等待第一个周期结束后再执行,适合那些不需要立即响应,但需要持续反馈的场景。

javascript
function throttle(fn, delay) {
  let timer = null;

  return function(...args) {
    // 如果定时器不存在,则设置一个
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null; // 执行完毕后清空定时器
      }, delay);
    }
  };
}

// 使用示例:鼠标移动,每 200ms 更新一次坐标
document.addEventListener('mousemove', throttle((e) => {
  console.log(`坐标: ${e.clientX}, ${e.clientY}`);
}, 200));

混合版

结合了时间戳和定时器的优点。既能保证不错过初始状态,又能防止最后一次操作被忽略。

javascript
function throttle(fn, delay) {
  let timer = null;
  let previous = 0;

  return function(...args) {
    const now = Date.now();
    const remaining = delay - (now - previous);

    // 如果没有剩余时间(即时间到了),立即执行
    if (remaining <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(this, args);
      previous = now;
    } else if (!timer) {
      // 如果定时器不存在,设置一个定时器处理剩余时间
      timer = setTimeout(() => {
        fn.apply(this, args);
        previous = Date.now();
        timer = null;
      }, remaining);
    }
  };
}

React Hooks 版

在 React 中,我们需要确保节流函数在组件生命周期内保持稳定,避免重复创建。

javascript
import { useRef, useCallback } from 'react';

function useThrottle(callback, delay) {
  const lastRun = useRef(Date.now());
  const timeoutRef = useRef();

  const throttledFn = useCallback((...args) => {
    const now = Date.now();
    
    // 如果距离上次执行超过 delay,立即执行
    if (now - lastRun.current >= delay) {
      callback(...args);
      lastRun.current = now;
    } else {
      // 否则设置定时器,确保最后一次也能执行
      if (timeoutRef.current) clearTimeout(timeoutRef.current);
      
      timeoutRef.current = setTimeout(() => {
        callback(...args);
        lastRun.current = now;
        timeoutRef.current = null;
      }, delay - (now - lastRun.current));
    }
  }, [callback, delay]);

  return throttledFn;
}

// 使用示例
// const handleScroll = useThrottle(() => console.log('Scroll'), 200);
// window.addEventListener('scroll', handleScroll);