JavaScript 防抖与节流完整指南

1. 概念对比

1.1 防抖(Debounce)

  • 定义: 在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时
  • 场景:
  • 搜索框输入联想
  • 窗口大小调整
  • 表单验证
  • 按钮提交事件

1.2 节流(Throttle)

  • 定义: 规定在一个单位时间内,只能触发一次函数,如果这个单位时间内触发多次函数,只有一次生效
  • 场景:
  • 滚动事件处理
  • 页面resize事件
  • 射击游戏中的子弹发射
  • 表单快速提交

1.3 区别示意

// 防抖:等待一段时间后执行,期间重新触发会重新计时
Input Events:   │─A─B─C─ │────D──│──E──│
Debounced:      │────────│────D──│──E──│

// 节流:按照一定时间间隔执行
Input Events:   │─A─B─C─D─E─F─G─│
Throttled:      │─A─────D─────G─│

2. JavaScript 实现

2.1 防抖实现

// 基础版本
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func.apply(this, args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// 带立即执行选项的完整版本
function debounce(func, wait, immediate = false) {
  let timeout;
  return function executedFunction(...args) {
    const context = this;
    
    const later = () => {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };

    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);

    if (callNow) func.apply(context, args);
  };
}

2.2 节流实现

// 时间戳版本
function throttle(func, limit) {
  let lastCall = 0;
  return function executedFunction(...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      func.apply(this, args);
      lastCall = now;
    }
  };
}

// 定时器版本
function throttle(func, limit) {
  let inThrottle;
  return function executedFunction(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

3. 使用示例

3.1 防抖示例

// 搜索框输入防抖
const searchInput = document.getElementById('search');
const handleSearch = (event) => {
  console.log('Searching:', event.target.value);
};

const debouncedSearch = debounce(handleSearch, 500);
searchInput.addEventListener('input', debouncedSearch);

// 窗口调整防抖
const handleResize = () => {
  console.log('Window resized');
};

const debouncedResize = debounce(handleResize, 250);
window.addEventListener('resize', debouncedResize);

3.2 节流示例

// 滚动事件节流
const handleScroll = () => {
  console.log('Scroll position:', window.scrollY);
};

const throttledScroll = throttle(handleScroll, 250);
window.addEventListener('scroll', throttledScroll);

// 按钮点击节流
const button = document.getElementById('submit-btn');
const handleClick = () => {
  console.log('Button clicked');
};

const throttledClick = throttle(handleClick, 1000);
button.addEventListener('click', throttledClick);

4. TypeScript 实现

4.1 防抖 TypeScript 实现

// 防抖函数类型定义
type DebouncedFunction<T extends (...args: any[]) => any> = {
  (...args: Parameters<T>): void;
  cancel: () => void;
}

function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number,
  immediate: boolean = false
): DebouncedFunction<T> {
  let timeout: NodeJS.Timeout | null = null;

  function executedFunction(this: any, ...args: Parameters<T>): void {
    const context = this;

    const later = () => {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };

    const callNow = immediate && !timeout;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(later, wait);

    if (callNow) func.apply(context, args);
  }

  executedFunction.cancel = function() {
    if (timeout) {
      clearTimeout(timeout);
      timeout = null;
    }
  };

  return executedFunction;
}

4.2 节流 TypeScript 实现

// 节流函数类型定义
type ThrottledFunction<T extends (...args: any[]) => any> = {
  (...args: Parameters<T>): void;
  cancel: () => void;
}

function throttle<T extends (...args: any[]) => any>(
  func: T,
  limit: number
): ThrottledFunction<T> {
  let inThrottle: boolean = false;
  let lastTimeout: NodeJS.Timeout | null = null;

  function executedFunction(this: any, ...args: Parameters<T>): void {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      
      lastTimeout = setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  }

  executedFunction.cancel = function() {
    if (lastTimeout) {
      clearTimeout(lastTimeout);
      inThrottle = false;
    }
  };

  return executedFunction;
}

5. TypeScript 使用示例

5.1 React 组件中使用

import React, { useState, useCallback } from 'react';

interface SearchProps {
  onSearch: (term: string) => void;
}

const SearchComponent: React.FC<SearchProps> = ({ onSearch }) => {
  // 防抖搜索
  const debouncedSearch = useCallback(
    debounce((term: string) => {
      onSearch(term);
    }, 500),
    [onSearch]
  );

  return (
    <input
      type="text"
      onChange={(e) => debouncedSearch(e.target.value)}
      placeholder="Search..."
    />
  );
};

// 节流滚动
const ScrollComponent: React.FC = () => {
  const [scrollPosition, setScrollPosition] = useState(0);

  const throttledScroll = useCallback(
    throttle(() => {
      setScrollPosition(window.scrollY);
    }, 250),
    []
  );

  React.useEffect(() => {
    window.addEventListener('scroll', throttledScroll);
    return () => {
      window.removeEventListener('scroll', throttledScroll);
      throttledScroll.cancel();
    };
  }, [throttledScroll]);

  return <div>Scroll Position: {scrollPosition}</div>;
};

5.2 Vue 组件中使用

import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const searchTerm = ref('');
    
    const debouncedSearch = debounce((term: string) => {
      console.log('Searching for:', term);
    }, 500);

    const handleInput = (event: Event) => {
      const target = event.target as HTMLInputElement;
      searchTerm.value = target.value;
      debouncedSearch(target.value);
    };

    return {
      searchTerm,
      handleInput
    };
  }
});

6. 最佳实践

6.1 防抖使用建议

  1. 设置合适的延迟时间
  2. 考虑是否需要立即执行
  3. 注意清理工作
  4. 在组件卸载时取消防抖

6.2 节流使用建议

  1. 根据实际场景选择合适的时间间隔
  2. 考虑是否需要首次执行
  3. 注意内存泄漏
  4. 在高频事件中使用

6.3 选择建议

  • 选择防抖:
  • 连续的事件响应只需要执行最后一次
  • 需要等待用户输入完成后再执行
  • 选择节流:
  • 需要保持一定的执行频率
  • 需要限制事件触发频率

7. 性能考虑

  1. 内存使用
  • 避免在组件渲染时创建新的防抖/节流函数
  • 使用 useCallback 或 useMemo 缓存函数
  1. 执行时机
  • 考虑是否需要在首次触发时立即执行
  • 考虑是否需要在等待结束后执行最后一次
  1. 清理工作
  • 组件卸载时清除定时器
  • 取消未执行的回调
  1. 类型安全
  • 使用 TypeScript 确保类型安全
  • 正确处理 this 绑定和参数类型