JavaScript防抖与节流全解析

发布于:2025-05-18 ⋅ 阅读:(18) ⋅ 点赞:(0)

前言:为什么需要防抖和节流

在前端开发中,我们经常会遇到一些高频触发的事件,例如:

  • 浏览器窗口调整大小(resize)
  • 页面滚动(scroll)
  • 鼠标移动(mousemove)
  • 键盘输入(keyup、keydown)
  • 频繁点击按钮等

如果不加控制,这些事件处理函数可能会在短时间内被频繁调用,导致以下问题:

  1. 性能问题:函数频繁执行,特别是复杂计算或DOM操作,会导致页面卡顿
  2. 资源浪费:例如输入搜索时,频繁发送不必要的API请求
  3. 不良用户体验:例如按钮重复点击导致的重复提交表单

看一个具体例子:

// 不做任何处理的搜索输入框
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', function() {
   
  // 每次输入都会执行,即使用户正在快速输入
  console.log('执行搜索:', this.value);
  fetchSearchResults(this.value); // 发送请求获取搜索结果
});

在上面的例子中,当用户快速输入"JavaScript"这个词时,可能会依次触发以下请求:

  • 搜索"J"
  • 搜索"Ja"
  • 搜索"Jav"
  • 搜索"Java"
  • 搜索"JavaS"
  • 搜索"JavaScript"

显然,除了最后一个请求,前面的所有请求都是不必要的,这不仅浪费了网络资源,还可能导致服务器压力过大。

这就是为什么我们需要防抖和节流技术:它们帮助我们控制函数的执行频率,优化性能和用户体验。

基本概念与区别

防抖(Debounce)

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

形象比喻:电梯关门 - 当有人进入电梯后,电梯会等待一段时间再关门,如果在这段时间内又有人进入,电梯会重新计时等待,直到一段时间内没有人进入才会关门。

典型场景

  • 搜索框输入,等用户输入完毕后再发送请求
  • 窗口调整大小完成后执行重排重绘
  • 按钮提交事件防止重复提交

节流(Throttle)

概念:函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

形象比喻:水龙头控制水流 - 无论你如何快速地多次拧开水龙头,水流速度都不会超过水管的限制。

典型场景

  • 滚动事件处理
  • 射击游戏中的武器发射频率限制
  • 鼠标移动事件处理

关键区别

特性 防抖(Debounce) 节流(Throttle)
执行时机 在一段时间内没有再次触发事件后执行 在一段时间内只执行一次
适用场景 需要等待操作完全结束后执行 需要保持一定的执行频率
执行频率 不稳定,取决于事件触发频率和间隔 稳定,保证一定时间内执行一次
最后一次是否执行 延迟执行,一定会执行 可能不会执行最后一次(取决于实现)

下面通过可视化图表来理解两者的区别:

连续事件触发:
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
│ │ │ │ │ │ │ │ │ │
0 1 2 3 4 5 6 7 8 9 (时间轴)

防抖(延迟3秒):
─────────────────────→ (只在最后一次事件后等待3秒执行)
                    ↓
                    9+3=12
                    
节流(间隔3秒):
↓       ↓       ↓     (每隔3秒执行一次)
│       │       │
0       3       6     (时间轴)

防抖(Debounce)详解

1. 基本防抖函数实现

/**
 * 基础版防抖函数
 * @param {Function} func - 需要防抖的函数
 * @param {number} wait - 等待时间,单位毫秒
 * @returns {Function} - 防抖处理后的函数
 */
function debounce(func, wait) {
   
  let timeout;
  
  return function() {
   
    const context = this; // 保存this指向
    const args = arguments; // 保存传入的参数
    
    // 清除之前的定时器
    clearTimeout(timeout);
    
    // 设置新的定时器
    timeout = setTimeout(function() {
   
      func.apply(context, args);
    }, wait);
  };
}

2. 防抖函数的使用

// 定义一个可能频繁调用的函数
function handleSearch(searchTerm) {
   
  console.log('Searching for:', searchTerm);
  // 发送API请求等操作...
}

// 获取输入框元素
const searchInput = document.getElementById('search-input');

// 未使用防抖的版本 - 每次输入都会执行
/*
searchInput.addEventListener('input', function() {
  handleSearch(this.value);
});
*/

// 使用防抖后的版本 - 停止输入300毫秒后才执行
const debouncedSearch = debounce(function() {
   
  handleSearch(this.value);
}, 300);

searchInput.addEventListener('input', debouncedSearch);

3. 防抖函数的工作流程

以搜索框为例,当用户连续输入"hello"这个词:

时间轴: 0ms     100ms    200ms    300ms    400ms    700ms
操作:    h        e        l        l        o       (停止输入)
函数调用: 无       无       无       无       无        执行搜索"hello"

每次按键都会重置定时器,只有当用户停止输入300ms后,才会执行一次搜索。

4. 防抖函数进阶 - 立即执行选项

在某些场景下,我们可能希望第一次触发事件时立即执行函数,然后等待一段时间再允许执行下一次。例如点击提交按钮时,我们希望立即响应第一次点击,然后暂时禁用后续点击。

/**
 * 带立即执行选项的防抖函数
 * @param {Function} func - 需要防抖的函数
 * @param {number} wait - 等待时间,单位毫秒
 * @param {boolean} immediate - 是否立即执行
 * @returns {Function} - 防抖处理后的函数
 */
function debounce(func, wait, immediate) {
   
  let timeout;
  
  return function() {
   
    const context = this;
    const args = arguments;
    
    const later = function() {
   
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    
    const callNow = immediate && !timeout;
    
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    
    if (callNow) func.apply(context, args);
  };
}

// 使用立即执行的防抖 - 第一次点击立即响应,后续点击被防抖
const button = document.getElementById('submit-button');
const immediateDebounceClick = debounce(function() {
   
  console.log('Button clicked!');
  // 提交表单等操作...
}, 1000, true);

button.addEventListener('click', immediateDebounceClick);

节流(Throttle)详解

1. 基本节流函数实现

有两种常见的方式实现节流函数:时间戳法和定时器法。

时间戳法(第一次会立即执行)
/**
 * 时间戳实现的节流函数
 * @param {Function} func - 需要节流的函数
 * @param {number} wait - 等待时间,单位毫秒
 * @returns {Function} - 节流处理后的函数
 */
function throttle(func, wait) {
   
  let previous = 0; // 上一次执行的时间戳
  
  return function() {
   
    const now = Date.now(); // 当前时间戳
    const context = this;
    const args = arguments;
    
    // 如果当前时间与上一次执行时间差大于等待时间
    if (now - previous > wait) {
   
      func.apply(context, args);
      previous = now;
    }
  };
}
定时器法(第一次会延迟执行)

网站公告

今日签到

点亮在社区的每一天
去签到