Vue中文社区

JavaScript原生实战手册 · 防抖节流实现原理:让你的页面性能飞起来

搜索框输入卡顿?滚动事件拖垮性能?两个神奇的技术让你的页面瞬间丝滑!

在日常开发中,你是否遇到过这样的问题:用户在搜索框里快速输入时,页面变得卡顿?或者滚动页面时,滚动条不够流畅?这些问题的根源往往是同一个:事件触发太频繁了。今天我们就来学习两个性能优化的核心技术——防抖(debounce)和节流(throttle),用原生JavaScript轻松解决这些性能问题。

生活中的防抖和节流

在开始写代码之前,我们先用生活中的例子来理解这两个概念:

防抖:等电梯的智慧

想象你在等电梯,如果每有一个人按电梯按钮,电梯就立即关门出发,那效率会很低。实际上,电梯会等一小段时间(比如10秒),如果这期间没有新的人按按钮,才会关门出发。如果有人按了按钮,就重新计时10秒。

// 防抖的核心思想:等待静默期
// 用户停止输入300毫秒后,才执行搜索

节流:限速行驶的规则

再想象一下高速公路的限速标志,不管你想开多快,都必须按照限速要求行驶。比如限速120km/h,那你最快也只能这个速度,不能更快。

// 节流的核心思想:控制执行频率
// 不管滚动多频繁,最多每16毫秒执行一次(约60FPS)

为什么需要防抖和节流?

场景一:搜索框实时查询

假设用户要搜索"JavaScript"这个词:

// 没有防抖的情况
用户输入 'J' -> 发送请求查询 'J'
用户输入 'a' -> 发送请求查询 'Ja'
用户输入 'v' -> 发送请求查询 'Jav'
用户输入 'a' -> 发送请求查询 'Java'
// ... 总共发送了10次请求!

// 有防抖的情况
用户输入 'JavaScript' -> 等待300毫秒 -> 发送1次请求查询 'JavaScript'

这样就从10次请求减少到1次,性能提升显而易见!

场景二:页面滚动事件

// 没有节流的情况
用户滚动鼠标 -> 每1毫秒触发一次滚动事件
1秒钟可能触发1000次事件!浏览器承受不了

// 有节流的情况  
用户滚动鼠标 -> 每16毫秒最多触发一次
1秒钟最多触发60次,刚好是60FPS,流畅且高效

防抖功能的完整实现

现在我们来实现一个功能完整的防抖函数:

functiondebounce(func, delay, immediate = false) {
let timeoutId; // 用来存储定时器ID

returnfunction(...args) {
// 如果设置了立即执行,且当前没有等待中的调用
const callNow = immediate && !timeoutId;

// 清除之前的定时器(重新开始等待)
    clearTimeout(timeoutId);

// 设置新的定时器
    timeoutId = setTimeout(() => {
      timeoutId = null; // 重置定时器ID
if (!immediate) {
        func.apply(this, args); // 延迟执行
      }
    }, delay);

// 如果需要立即执行
if (callNow) {
      func.apply(this, args);
    }
  };
}

代码详解

让我们逐步理解这个实现:

1. 参数说明:

  • func:需要防抖的原函数
  • delay:延迟时间(毫秒)
  • immediate:是否立即执行(可选)

2. 核心机制:

// 每次调用都会清除之前的定时器
clearTimeout(timeoutId);

// 然后设置新的定时器
timeoutId = setTimeout(() => {
// delay毫秒后执行
}, delay);

3. 立即执行模式:

// immediate = true:第一次立即执行,后续的调用需要等待
// immediate = false:每次都等待delay后执行

实际使用示例

// 搜索框防抖
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(function(query) {
console.log('正在搜索:', query);
// 这里发送API请求
  fetch(`/api/search?q=${query}`)
    .then(response => response.json())
    .then(data => {
// 处理搜索结果
      displaySearchResults(data);
    });
}, 300);

searchInput.addEventListener('input', function(e) {
  debouncedSearch(e.target.value);
});

// 窗口大小调整防抖
const debouncedResize = debounce(function() {
console.log('窗口大小改变了:', window.innerWidth, window.innerHeight);
// 重新计算布局
  recalculateLayout();
}, 250);

window.addEventListener('resize', debouncedResize);

// 保存文档防抖(立即执行模式)
const debouncedSave = debounce(function(document) {
console.log('保存文档...');
  saveDocument(document);
}, 1000, true); // 立即保存,但1秒内的重复保存会被忽略

节流功能的完整实现

节流的实现相对复杂一些,因为需要精确控制执行频率:

functionthrottle(func, limit) {
let inThrottle; // 是否在节流期内
let lastFunc;   // 最后一次调用的定时器
let lastRan;    // 最后一次执行的时间

returnfunction(...args) {
if (!inThrottle) {
// 如果不在节流期,立即执行
      func.apply(this, args);
      lastRan = Date.now();
      inThrottle = true;
    } else {
// 如果在节流期,清除之前的定时器,设置新的
      clearTimeout(lastFunc);
      lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
          func.apply(this, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  };
}

节流实现原理解析

1. 第一次调用:

if (!inThrottle) {
  func.apply(this, args); // 立即执行
  lastRan = Date.now();   // 记录执行时间
  inThrottle = true;      // 进入节流期
}

2. 节流期内的调用:

// 计算还需要等待多长时间
const remainingTime = limit - (Date.now() - lastRan);

// 设置定时器,在剩余时间后执行
setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
    func.apply(this, args);
    lastRan = Date.now();
  }
}, remainingTime);

这样确保了函数执行间隔至少为limit毫秒。

节流使用示例

// 滚动事件节流
const throttledScroll = throttle(function() {
const scrollPercent = 
    (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;

// 更新进度条
document.getElementById('progress').style.width = `${scrollPercent}%`;

// 检查是否需要懒加载图片
  checkLazyLoadImages();
}, 16); // 约60FPS

window.addEventListener('scroll', throttledScroll);

// 鼠标移动事件节流
const throttledMouseMove = throttle(function(e) {
// 更新鼠标指针效果
  updateCursor(e.clientX, e.clientY);
}, 33); // 约30FPS

document.addEventListener('mousemove', throttledMouseMove);

// API请求频率限制
const throttledApiCall = throttle(function(data) {
console.log('发送API请求');
  fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(data)
  });
}, 1000); // 最多每秒一次请求

防抖 vs 节流:何时使用哪个?

使用防抖的场景

✅ 搜索框输入:用户停止输入后再搜索 ✅ 表单验证:用户停止输入后再验证 ✅ 窗口大小调整:调整完成后再重新计算布局 ✅ 按钮重复点击:防止用户快速多次点击

// 表单验证防抖示例
const validateField = debounce(function(field, value) {
if (value.length < 3) {
    showError(field, '至少输入3个字符');
  } else {
    clearError(field);
// 可能的异步验证(如检查用户名是否存在)
    checkFieldAvailability(field, value);
  }
}, 500);

document.getElementById('username').addEventListener('input', function(e) {
  validateField('username', e.target.value);
});

使用节流的场景

✅ 滚动事件:控制滚动处理频率 ✅ 鼠标移动:限制动画更新频率 ✅ API请求频率限制:避免请求过于频繁 ✅ 游戏操作:控制射击频率等

// 无限滚动加载节流示例
const throttledInfiniteScroll = throttle(function() {
const scrollPosition = window.scrollY + window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;

// 距离底部100px时开始加载
if (scrollPosition >= documentHeight - 100) {
    loadMoreContent();
  }
}, 100);

window.addEventListener('scroll', throttledInfiniteScroll);

性能对比测试

让我们看看防抖和节流的性能提升效果:

// 性能测试函数
functionperformanceTest() {
let normalCount = 0;
let debouncedCount = 0;
let throttledCount = 0;

// 普通函数
const normalFunc = () => normalCount++;

// 防抖函数
const debouncedFunc = debounce(() => debouncedCount++, 100);

// 节流函数
const throttledFunc = throttle(() => throttledCount++, 100);

// 模拟快速调用(比如用户快速输入)
for (let i = 0; i < 100; i++) {
    setTimeout(() => {
      normalFunc();
      debouncedFunc();
      throttledFunc();
    }, i * 10); // 每10毫秒调用一次
  }

// 2秒后查看结果
  setTimeout(() => {
console.log('普通函数执行次数:', normalCount); // 100次
console.log('防抖函数执行次数:', debouncedCount); // 1次
console.log('节流函数执行次数:', throttledCount); // 约10次
  }, 2000);
}

performanceTest();

实际项目中的高级应用

智能搜索组件

classSmartSearch{
constructor(inputElement, options = {}) {
this.input = inputElement;
this.options = {
minLength: 2,
delay: 300,
maxResults: 10,
      ...options
    };

this.cache = newMap(); // 缓存搜索结果
this.abortController = null; // 用于取消请求

this.init();
  }

  init() {
// 使用防抖处理搜索
const debouncedSearch = debounce((query) => {
this.performSearch(query);
    }, this.options.delay);

this.input.addEventListener('input', (e) => {
const query = e.target.value.trim();

if (query.length >= this.options.minLength) {
        debouncedSearch(query);
      } else {
this.clearResults();
      }
    });
  }

async performSearch(query) {
// 检查缓存
if (this.cache.has(query)) {
this.displayResults(this.cache.get(query));
return;
    }

// 取消之前的请求
if (this.abortController) {
this.abortController.abort();
    }

this.abortController = new AbortController();

try {
const response = await fetch(`/api/search?q=${query}&limit=${this.options.maxResults}`, {
signal: this.abortController.signal
      });

const results = await response.json();

// 缓存结果
this.cache.set(query, results);

this.displayResults(results);
    } catch (error) {
if (error.name !== 'AbortError') {
console.error('搜索失败:', error);
      }
    }
  }

  displayResults(results) {
// 显示搜索结果的逻辑
console.log('搜索结果:', results);
  }

  clearResults() {
// 清空搜索结果的逻辑
console.log('清空结果');
  }
}

// 使用示例
const searchInput = document.getElementById('search');
const smartSearch = new SmartSearch(searchInput, {
minLength: 2,
delay: 300,
maxResults: 8
});

滚动动画控制器

classScrollAnimationController{
constructor() {
this.elements = newMap(); // 存储需要动画的元素
this.isScrolling = false;

this.init();
  }

  init() {
// 使用节流控制滚动事件
const throttledScroll = throttle(() => {
this.handleScroll();
    }, 16); // 60FPS

// 使用防抖检测滚动结束
const debouncedScrollEnd = debounce(() => {
this.isScrolling = false;
this.onScrollEnd();
    }, 150);

window.addEventListener('scroll', () => {
this.isScrolling = true;
      throttledScroll();
      debouncedScrollEnd();
    });
  }

  addElement(element, animationType = 'fadeIn') {
this.elements.set(element, {
type: animationType,
animated: false,
threshold: 0.1
    });
  }

  handleScroll() {
const scrollY = window.scrollY;
const windowHeight = window.innerHeight;

this.elements.forEach((config, element) => {
if (config.animated) return;

const elementTop = element.offsetTop;
const elementHeight = element.offsetHeight;

// 判断元素是否进入视口
if (scrollY + windowHeight > elementTop + elementHeight * config.threshold) {
this.animateElement(element, config);
        config.animated = true;
      }
    });

// 更新进度条
this.updateProgressBar();
  }

  animateElement(element, config) {
switch (config.type) {
case'fadeIn':
        element.style.opacity = '0';
        element.style.transform = 'translateY(30px)';
        element.style.transition = 'opacity 0.6s ease, transform 0.6s ease';

        requestAnimationFrame(() => {
          element.style.opacity = '1';
          element.style.transform = 'translateY(0)';
        });
break;

case'slideIn':
        element.style.transform = 'translateX(-100%)';
        element.style.transition = 'transform 0.8s ease';

        requestAnimationFrame(() => {
          element.style.transform = 'translateX(0)';
        });
break;
    }
  }

  updateProgressBar() {
const progress = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
const progressBar = document.getElementById('scroll-progress');

if (progressBar) {
      progressBar.style.width = `${Math.min(progress, 100)}%`;
    }
  }

  onScrollEnd() {
console.log('滚动结束,可以执行一些收尾工作');
// 比如保存滚动位置到localStorage
    localStorage.setItem('scrollPosition', window.scrollY);
  }
}

// 使用示例
const scrollController = new ScrollAnimationController();

// 为页面元素添加滚动动画
document.querySelectorAll('.animate-on-scroll').forEach(element => {
  scrollController.addElement(element, 'fadeIn');
});

注意事项和最佳实践

1. 选择合适的延迟时间

// 不同场景的推荐延迟时间
const delays = {
search: 300,        // 搜索:300毫秒,给用户思考时间
resize: 250,        // 窗口调整:250毫秒,平衡响应性和性能
input: 500,         // 表单验证:500毫秒,避免过于频繁的验证
scroll: 16,         // 滚动节流:16毫秒,约60FPS
mouse: 33,          // 鼠标移动:33毫秒,约30FPS
api: 1000,         // API请求:1秒,避免服务器压力
};

2. 内存泄漏预防

// 清理定时器,避免内存泄漏
classComponentWithDebounce{
constructor() {
this.debouncedMethod = debounce(this.handleInput.bind(this), 300);
  }

  handleInput() {
// 处理输入
  }

  destroy() {
// 组件销毁时清理
if (this.debouncedMethod.cancel) {
this.debouncedMethod.cancel();
    }
  }
}

3. 增强版防抖和节流

// 增强版防抖:支持取消和立即执行
functionenhancedDebounce(func, delay, immediate = false) {
let timeoutId;

const debounced = function(...args) {
const callNow = immediate && !timeoutId;

    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      timeoutId = null;
if (!immediate) {
        func.apply(this, args);
      }
    }, delay);

if (callNow) {
      func.apply(this, args);
    }
  };

// 添加取消方法
  debounced.cancel = function() {
    clearTimeout(timeoutId);
    timeoutId = null;
  };

// 添加立即执行方法
  debounced.flush = function() {
if (timeoutId) {
      clearTimeout(timeoutId);
      func.apply(this, arguments);
      timeoutId = null;
    }
  };

return debounced;
}

总结

防抖和节流是前端性能优化的两大利器:

防抖适用于:

  • 用户输入场景(搜索、验证)
  • 窗口调整
  • 按钮防重复点击
  • 需要等待用户"完成操作"的场景

节流适用于:

  • 滚动事件处理
  • 鼠标移动事件
  • API调用频率限制
  • 需要"定期执行"的场景

掌握了这两个技术,你就能让页面在各种高频事件下都保持丝滑的性能表现。更重要的是,这些都是用纯原生JavaScript实现的,不需要任何外部依赖,可以放心在任何项目中使用。

记住:性能优化不是一蹴而就的,需要根据具体场景选择合适的技术方案。防抖和节流就是你工具箱中的两把利剑,合理使用它们,让你的页面性能真正"飞起来"!