编程 防抖(Debounce)与节流(Throttle):从传统实现到现代化方案

2025-08-15 15:52:16 +0800 CST views 13

防抖(Debounce)与节流(Throttle):从传统实现到现代化方案

在前端开发中,高频触发事件(如滚动、窗口调整、输入等)可能导致性能问题。**防抖(Debounce)节流(Throttle)**是两种常用的优化技术,用于限制事件处理函数的执行频率。

随着 JavaScript 的发展,我们已经可以用更简洁、现代化的方式实现防抖和节流,而无需写复杂的封装函数。


一、传统实现方式回顾

1. 防抖(Debounce)

防抖的核心理念:事件在触发后一段时间内不再触发,才执行回调

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用示例
const handleSearch = debounce(function(e) {
  console.log('搜索内容:', e.target.value);
}, 300);

searchInput.addEventListener('input', handleSearch);

2. 节流(Throttle)

节流的核心理念:事件在固定时间间隔内只触发一次

function throttle(fn, delay) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

// 使用示例
const handleScroll = throttle(function() {
  console.log('页面滚动');
}, 200);

window.addEventListener('scroll', handleScroll);

二、现代化实现方式

1. 函数装饰器(Decorator)

ECMAScript 提案中的函数装饰器可以增强函数行为而不修改原始函数。结合装饰器可以一行实现防抖或节流(需 Babel / TypeScript 转译):

function debounceDecorator(delay) {
  return function(target, key, descriptor) {
    const original = descriptor.value;
    let timer = null;
    descriptor.value = function(...args) {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => original.apply(this, args), delay);
    };
    return descriptor;
  };
}

class Search {
  @debounceDecorator(300)
  handleInput(e) {
    console.log('搜索内容:', e.target.value);
  }
}

2. requestAnimationFrame 节流

requestAnimationFrame 可将事件处理函数与浏览器渲染周期同步,实现高效节流:

let ticking = false;
window.addEventListener('scroll', function() {
  if (!ticking) {
    requestAnimationFrame(() => {
      console.log('页面滚动');
      ticking = false;
    });
    ticking = true;
  }
});

3. AbortController 防抖

结合 AbortController 可以清晰地管理取消机制,实现防抖:

const controller = new AbortController();
const signal = controller.signal;

input.addEventListener('input', (e) => {
  controller.abort(); // 取消上一次
  controller = new AbortController();
  fetch('/search?q=' + e.target.value, { signal: controller.signal });
});

4. Web Streams API

Web Streams API 提供声明式方法实现防抖/节流,适合流式数据处理,但 API 相对复杂,兼容性有限。

5. 第三方库(Lodash / Underscore)

最简单的方式:

import { debounce, throttle } from 'lodash';

// 防抖
element.addEventListener('input', debounce(handleInput, 300));

// 节流
window.addEventListener('scroll', throttle(handleScroll, 200));

三、实际应用场景

场景使用方式说明
搜索输入框防抖避免每次输入都发送请求
窗口大小调整节流限制布局计算频率
无限滚动节流控制加载新内容频率
游戏按键输入防抖/节流防止响应过于频繁
拖拽元素节流保持平滑性能

四、性能对比

实现方式优点缺点
传统函数封装兼容性好,灵活代码冗长,需要手动管理
装饰器语法简洁、声明式需转译,兼容性问题
requestAnimationFrame与浏览器渲染周期同步仅适合视觉相关操作
AbortController清晰管理取消机制新 API,需 polyfill
Web Streams声明式、功能强大API 复杂,兼容性有限
第三方库简单、稳定增加依赖和体积

五、总结

防抖和节流是提升用户体验和性能的关键技术。随着 JavaScript 的发展,我们有多种方式选择:

  • 现代项目:可以使用装饰器、Web API 提供的简洁方案
  • 广泛兼容项目:传统函数封装或成熟第三方库仍然可靠

掌握这些技术,让你的前端应用在高频事件场景下依然保持流畅和高性能。


如果你需要,我可以帮你写一个 完整的示例页面,同时展示 传统防抖/节流、装饰器实现、requestAnimationFrame、Lodash 的对比效果,让读者直观看到差异。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖/节流对比示例</title>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<style>
  body { font-family: Arial, sans-serif; padding: 20px; }
  input { margin-bottom: 20px; padding: 5px; width: 300px; }
  .log { margin-top: 10px; max-height: 200px; overflow-y: auto; background: #f5f5f5; padding: 10px; border: 1px solid #ddd; }
  .section { margin-bottom: 40px; }
  h2 { margin-bottom: 10px; }
</style>
</head>
<body>

<h1>防抖 / 节流示例对比</h1>

<div class="section">
  <h2>1. 输入框防抖 (传统函数)</h2>
  <input id="input-debounce" placeholder="输入文字触发防抖">
  <div class="log" id="log-debounce"></div>
</div>

<div class="section">
  <h2>2. 页面滚动节流 (传统函数)</h2>
  <div style="height:300px; overflow-y:scroll; border:1px solid #ccc;" id="scroll-area">
    <div style="height:1000px;">滚动区域</div>
  </div>
  <div class="log" id="log-throttle"></div>
</div>

<div class="section">
  <h2>3. 输入框防抖 (Lodash)</h2>
  <input id="input-lodash-debounce" placeholder="Lodash 防抖">
  <div class="log" id="log-lodash-debounce"></div>
</div>

<div class="section">
  <h2>4. 页面滚动节流 (requestAnimationFrame)</h2>
  <div style="height:300px; overflow-y:scroll; border:1px solid #ccc;" id="scroll-area-raf">
    <div style="height:1000px;">滚动区域 RAF</div>
  </div>
  <div class="log" id="log-raf"></div>
</div>

<script>
// -------------------- 传统防抖 --------------------
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const logDebounce = document.getElementById('log-debounce');
document.getElementById('input-debounce').addEventListener('input',
  debounce((e) => {
    const msg = `防抖触发: ${e.target.value}`;
    logDebounce.innerHTML += msg + '<br>';
    logDebounce.scrollTop = logDebounce.scrollHeight;
  }, 500)
);

// -------------------- 传统节流 --------------------
function throttle(fn, delay) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

const logThrottle = document.getElementById('log-throttle');
document.getElementById('scroll-area').addEventListener('scroll',
  throttle(() => {
    const msg = `节流触发: 滚动位置 ${document.getElementById('scroll-area').scrollTop}`;
    logThrottle.innerHTML += msg + '<br>';
    logThrottle.scrollTop = logThrottle.scrollHeight;
  }, 200)
);

// -------------------- Lodash 防抖 --------------------
const logLodashDebounce = document.getElementById('log-lodash-debounce');
document.getElementById('input-lodash-debounce').addEventListener('input',
  _.debounce((e) => {
    const msg = `Lodash 防抖触发: ${e.target.value}`;
    logLodashDebounce.innerHTML += msg + '<br>';
    logLodashDebounce.scrollTop = logLodashDebounce.scrollHeight;
  }, 500)
);

// -------------------- requestAnimationFrame 节流 --------------------
const logRaf = document.getElementById('log-raf');
let ticking = false;
document.getElementById('scroll-area-raf').addEventListener('scroll', function() {
  if (!ticking) {
    requestAnimationFrame(() => {
      const msg = `RAF 节流触发: 滚动位置 ${document.getElementById('scroll-area-raf').scrollTop}`;
      logRaf.innerHTML += msg + '<br>';
      logRaf.scrollTop = logRaf.scrollHeight;
      ticking = false;
    });
    ticking = true;
  }
});
</script>

</body>
</html>

复制全文 生成海报 前端开发 性能优化 JavaScript

推荐文章

从Go开发者的视角看Rust
2024-11-18 11:49:49 +0800 CST
curl错误代码表
2024-11-17 09:34:46 +0800 CST
php strpos查找字符串性能对比
2024-11-19 08:15:16 +0800 CST
在 Vue 3 中如何创建和使用插件?
2024-11-18 13:42:12 +0800 CST
四舍五入五成双
2024-11-17 05:01:29 +0800 CST
JavaScript设计模式:适配器模式
2024-11-18 17:51:43 +0800 CST
Vue3中的虚拟滚动有哪些改进?
2024-11-18 23:58:18 +0800 CST
Go配置镜像源代理
2024-11-19 09:10:35 +0800 CST
一个有趣的进度条
2024-11-19 09:56:04 +0800 CST
robots.txt 的写法及用法
2024-11-19 01:44:21 +0800 CST
Vue 3 中的 Fragments 是什么?
2024-11-17 17:05:46 +0800 CST
38个实用的JavaScript技巧
2024-11-19 07:42:44 +0800 CST
最全面的 `history` 命令指南
2024-11-18 21:32:45 +0800 CST
程序员茄子在线接单