概述

节流(Throttle)和防抖(Debounce)是JavaScript中常用的性能优化技术,用于控制函数的执行频率,避免因频繁触发而导致的性能问题。

防抖(Debounce)

概念

防抖是指在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

应用场景

  • 搜索框输入搜索
  • 窗口大小调整(resize)
  • 表单验证
  • 按钮点击防止重复提交

手动实现

// 基础版防抖
function debounce(func, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// 立即执行版防抖
function debounceImmediate(func, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    const callNow = !timer;
    timer = setTimeout(() => {
      timer = null;
    }, delay);
    if (callNow) func.apply(this, args);
  };
}

// 使用示例
const searchInput = document.getElementById('search');
const handleSearch = debounce(function(e) {
  console.log('搜索内容:', e.target.value);
  // 发送搜索请求
}, 300);

searchInput.addEventListener('input', handleSearch);

节流(Throttle)

概念

节流是指规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

应用场景

  • 页面滚动(scroll)
  • 鼠标移动(mousemove)
  • 窗口大小调整(resize)
  • 按钮频繁点击

手动实现

// 时间戳版节流
function throttleTimestamp(func, delay) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

// 定时器版节流
function throttleTimer(func, delay) {
  let timer = null;
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

// 结合版节流(首次立即执行,末尾也执行)
function throttle(func, delay) {
  let timer = null;
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    const remaining = delay - (now - lastTime);
    
    if (remaining <= 0 || remaining > delay) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      func.apply(this, args);
      lastTime = now;
    } else if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        lastTime = Date.now();
        timer = null;
      }, remaining);
    }
  };
}

// 使用示例
const handleScroll = throttle(function() {
  console.log('页面滚动位置:', window.scrollY);
  // 执行滚动相关操作
}, 100);

window.addEventListener('scroll', handleScroll);

第三方库使用

Lodash

安装

npm install lodash
# 或者使用CDN
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

防抖使用

import { debounce } from 'lodash';

// 基础用法
const debouncedSearch = debounce((value) => {
  console.log('搜索:', value);
}, 300);

// 带配置选项
const debouncedSearchWithOptions = debounce((value) => {
  console.log('搜索:', value);
}, 300, {
  leading: false,    // 是否在延迟开始前调用函数
  trailing: true,    // 是否在延迟结束后调用函数
  maxWait: 1000      // 设置 func 允许被延迟的最大值
});

// 取消防抖
debouncedSearch.cancel();

// 立即执行
debouncedSearch.flush();

节流使用

import { throttle } from 'lodash';

// 基础用法
const throttledScroll = throttle(() => {
  console.log('滚动事件');
}, 100);

// 带配置选项
const throttledScrollWithOptions = throttle(() => {
  console.log('滚动事件');
}, 100, {
  leading: true,     // 是否在延迟开始前调用函数
  trailing: true,    // 是否在延迟结束后调用函数
  maxWait: 1000      // 设置 func 允许被延迟的最大值
});

// 取消节流
throttledScroll.cancel();

// 立即执行
throttledScroll.flush();

XE-Utils

安装

npm install xe-utils
# 或者使用CDN
<script src="https://cdn.jsdelivr.net/npm/xe-utils"></script>

防抖使用

import XEUtils from 'xe-utils';

// 基础用法
const debouncedFn = XEUtils.debounce((value) => {
  console.log('防抖执行:', value);
}, 300);

// 立即执行
debouncedFn('test');

// 取消
debouncedFn.cancel();

// 立即执行待处理的方法
debouncedFn.flush();

节流使用

import XEUtils from 'xe-utils';

// 基础用法
const throttledFn = XEUtils.throttle(() => {
  console.log('节流执行');
}, 100);

// 立即执行
throttledFn();

// 取消
throttledFn.cancel();

// 立即执行待处理的方法
throttledFn.flush();

React Hook 封装

useDebounce Hook

import { useCallback, useRef } from 'react';

export function useDebounce(callback, delay) {
  const timerRef = useRef(null);

  const debouncedCallback = useCallback((...args) => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
    timerRef.current = setTimeout(() => {
      callback(...args);
    }, delay);
  }, [callback, delay]);

  const cancel = useCallback(() => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
  }, []);

  return { debouncedCallback, cancel };
}

// 使用示例
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const { debouncedCallback } = useDebounce((term) => {
    // 执行搜索逻辑
    console.log('搜索:', term);
  }, 300);

  const handleChange = (e) => {
    const value = e.target.value;
    setSearchTerm(value);
    debouncedCallback(value);
  };

  return <input onChange={handleChange} value={searchTerm} />;
}

useThrottle Hook

import { useCallback, useRef } from 'react';

export function useThrottle(callback, delay) {
  const timerRef = useRef(null);
  const lastExecRef = useRef(0);

  const throttledCallback = useCallback((...args) => {
    const now = Date.now();
    const timeSinceLastExec = now - lastExecRef.current;

    if (timeSinceLastExec >= delay) {
      callback(...args);
      lastExecRef.current = now;
    } else if (!timerRef.current) {
      timerRef.current = setTimeout(() => {
        callback(...args);
        lastExecRef.current = Date.now();
        timerRef.current = null;
      }, delay - timeSinceLastExec);
    }
  }, [callback, delay]);

  const cancel = useCallback(() => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
      timerRef.current = null;
    }
  }, []);

  return { throttledCallback, cancel };
}

// 使用示例
function ScrollComponent() {
  const { throttledCallback } = useThrottle(() => {
    console.log('滚动处理');
  }, 100);

  useEffect(() => {
    const handleScroll = () => throttledCallback();
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [throttledCallback]);

  return <div>滚动内容</div>;
}

Vue 指令封装

// 防抖指令
const debounceDirective = {
  inserted(el, binding) {
    const { value, arg = 300 } = binding;
    let timer = null;
    
    el.addEventListener('click', () => {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => {
        value();
      }, arg);
    });
  }
};

// 节流指令
const throttleDirective = {
  inserted(el, binding) {
    const { value, arg = 300 } = binding;
    let timer = null;
    let lastTime = 0;
    
    el.addEventListener('click', () => {
      const now = Date.now();
      if (now - lastTime >= arg) {
        value();
        lastTime = now;
      } else if (!timer) {
        timer = setTimeout(() => {
          value();
          lastTime = Date.now();
          timer = null;
        }, arg - (now - lastTime));
      }
    });
  }
};

// Vue 3 使用
app.directive('debounce', debounceDirective);
app.directive('throttle', throttleDirective);

性能对比

方法 优点 缺点 适用场景
防抖 减少不必要的执行,节省资源 响应延迟 搜索输入、表单验证
节流 保证定期执行,响应及时 可能执行多次 滚动事件、鼠标移动

最佳实践

  1. 选择合适的延迟时间:根据业务需求调整,通常防抖300-500ms,节流100-200ms
  2. 及时清理:在组件卸载时取消未执行的函数
  3. 考虑内存泄漏:避免在循环中创建防抖/节流函数
  4. 测试边界情况:测试快速连续触发和取消的场景
  5. 优先使用成熟的第三方库:Lodash等库经过充分测试,功能更完善

总结

节流和防抖是前端性能优化的重要手段,合理使用可以显著提升用户体验。在实际开发中,建议优先使用成熟的第三方库实现,只有在特殊需求时才考虑手动实现。