news 2026/6/9 19:24:43

防抖(Debounce)与节流(Throttle)的源码级实现:支持立即执行与取消功能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
防抖(Debounce)与节流(Throttle)的源码级实现:支持立即执行与取消功能

防抖(Debounce)与节流(Throttle)的源码级实现:支持立即执行与取消功能

大家好,今天我们来深入探讨两个在前端开发中极其重要但又常被误解的性能优化技术:防抖(Debounce)节流(Throttle)。它们广泛应用于搜索框输入、窗口缩放、滚动事件监听等高频触发场景,目的是减少不必要的函数调用,提升用户体验和系统性能。

本讲座将从理论出发,逐步推导出它们的核心逻辑,并提供完整可运行的源码级实现,包括:

  • 支持“立即执行”选项
  • 支持“取消”操作(即手动中断定时器)
  • 代码结构清晰、注释详尽、易于扩展

一、什么是防抖和节流?

1. 防抖(Debounce)

定义:在一段时间内连续触发事件时,只在最后一次触发后等待指定延迟时间再执行一次回调函数。

适用场景:

  • 用户在搜索框中输入内容,希望每停顿1秒后再发起请求。
  • 实时表单校验,避免频繁 API 调用。

核心思想:延时执行 + 清除旧任务

2. 节流(Throttle)

定义:规定一个时间段内最多只执行一次回调函数,无论期间触发多少次事件。

适用场景:

  • 窗口 resize 或 scroll 事件处理,防止页面卡顿。
  • 滚动加载更多数据,限制频率。

核心思想:固定间隔执行 + 控制节奏


二、为什么需要防抖和节流?

想象这样一个场景:

window.addEventListener('scroll', () => { console.log('滚动了'); });

如果用户快速滚动页面,可能会触发成百上千次scroll事件。每次打印日志可能只是调试用途,但如果换成请求接口、重绘 DOM 或计算复杂逻辑,就会造成严重的性能问题 —— 浏览器卡顿甚至崩溃。

解决方案就是使用Debounce / Throttle来控制执行频率。


三、核心区别对比(表格总结)

特性防抖(Debounce)节流(Throttle)
触发方式最后一次触发后延迟执行固定周期内只执行一次
是否立即执行可配置不会立即执行(除非设置立即执行)
执行时机停止触发后才执行每隔固定时间执行
适用场景输入框搜索、实时验证滚动/缩放监听、鼠标移动
是否可取消支持支持(通过 clearTimeout)

注意:两者都能通过clearTimeout实现取消功能!这是很多初学者忽略的关键点。


四、源码级实现详解(带注释)

我们分别实现两个高阶函数:debouncethrottle,并支持以下特性:

  • immediate: boolean—— 是否立即执行
  • cancel(): void—— 取消当前待执行的任务
  • 返回值是一个函数对象,包含上述方法

1. 防抖(Debounce)实现

function debounce(fn, delay = 300, immediate = false) { let timeoutId = null; function debounced(...args) { // 如果已经存在定时器,则清除它(防抖核心逻辑) if (timeoutId) clearTimeout(timeoutId); // 如果设置了立即执行且是第一次调用 if (immediate && !timeoutId) { fn.apply(this, args); // 立即执行 } // 设置新的定时器,在 delay 后执行 fn timeoutId = setTimeout(() => { timeoutId = null; // 清空状态 if (!immediate) { fn.apply(this, args); } }, delay); } // 添加 cancel 方法用于取消当前待执行的任务 debounced.cancel = function() { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } }; return debounced; }

关键点说明:

  • timeoutId是全局唯一标识符,用于管理定时器。
  • immediate控制是否在首次调用时立刻执行。
  • cancel()方法允许外部主动终止未完成的防抖任务(比如组件卸载时)。
  • 使用apply保证this上下文正确传递给原函数。
示例演示:
const searchHandler = debounce((query) => { console.log(`搜索 "${query}"`); }, 500, true); // 立即执行模式 searchHandler("a"); // 立即输出:搜索 "a" searchHandler("ab"); // 清除上一个定时器,重新计时 searchHandler("abc"); // 再次清空,继续等待 // 500ms 后无新调用 → 输出:搜索 "abc" searchHandler.cancel(); // 主动取消最后的等待任务

2. 节流(Throttle)实现

function throttle(fn, delay = 300, options = {}) { const { leading = true, trailing = true } = options; let lastTime = 0; let timeoutId = null; function throttled(...args) { const now = Date.now(); // 第一次调用或距离上次执行超过 delay if (lastTime === 0 || now - lastTime >= delay) { if (leading) { fn.apply(this, args); } lastTime = now; } else { // 如果不是 leading,且 trailing 为 true,则设置尾部延迟执行 if (trailing && !timeoutId) { timeoutId = setTimeout(() => { timeoutId = null; fn.apply(this, args); lastTime = Date.now(); }, delay - (now - lastTime)); } } } // 取消方法:清除定时器并重置状态 throttled.cancel = function() { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } lastTime = 0; }; return throttled; }

关键点说明:

  • leading控制是否在第一次调用时立即执行(默认 true)。
  • trailing控制是否在最后一次调用后延迟执行(默认 true)。
  • lastTime记录上次执行的时间戳,用于判断是否满足间隔条件。
  • trailing的实现稍微复杂一点:当事件密集发生时,最后一个事件会被延迟执行(模拟“尾部执行”行为)。
示例演示:
const handleScroll = throttle((event) => { console.log("滚动事件触发", event.type); }, 1000, { leading: true, trailing: true }); // 快速触发多次 scroll handleScroll({ type: 'scroll' }); // 立即执行 handleScroll({ type: 'scroll' }); // 忽略(未满1s) handleScroll({ type: 'scroll' }); // 忽略 setTimeout(() => handleScroll({ type: 'scroll' }), 800); // 还没到1s,不会执行 setTimeout(() => handleScroll({ type: 'scroll' }), 1200); // 超过1s,再次执行 // 如果想取消:handleScroll.cancel();

五、进阶技巧:封装为类(更易管理)

有时候我们需要对多个防抖/节流函数进行统一管理和清理(如 React 组件卸载时)。我们可以将其封装为类:

class Debouncer { constructor(delay = 300, immediate = false) { this.delay = delay; this.immediate = immediate; this.timeoutId = null; } run(fn, ...args) { if (this.timeoutId) clearTimeout(this.timeoutId); if (this.immediate && !this.timeoutId) { fn.apply(this, args); } this.timeoutId = setTimeout(() => { this.timeoutId = null; if (!this.immediate) { fn.apply(this, args); } }, this.delay); } cancel() { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } } } class Throttler { constructor(delay = 300, options = {}) { this.delay = delay; this.leading = options.leading ?? true; this.trailing = options.trailing ?? true; this.lastTime = 0; this.timeoutId = null; } run(fn, ...args) { const now = Date.now(); if (this.lastTime === 0 || now - this.lastTime >= this.delay) { if (this.leading) fn.apply(this, args); this.lastTime = now; } else { if (this.trailing && !this.timeoutId) { this.timeoutId = setTimeout(() => { this.timeoutId = null; fn.apply(this, args); this.lastTime = Date.now(); }, this.delay - (now - this.lastTime)); } } } cancel() { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } this.lastTime = 0; } }

这样可以在组件中轻松维护多个任务:

class MyComponent { constructor() { this.debounceSearch = new Debouncer(500, true); this.throttleResize = new Throttler(300, { leading: true, trailing: true }); } onSearch(query) { this.debounceSearch.run(console.log, query); } onResize(event) { this.throttleResize.run(console.log, event); } destroy() { this.debounceSearch.cancel(); this.throttleResize.cancel(); } }

六、常见误区澄清

误区正确理解
“防抖一定会延迟执行”错!若设置了immediate=true,首次调用会立即执行
“节流就是每隔一段时间执行一次”不准确!还要看leadingtrailing参数如何配置
“防抖适合所有高频事件”不一定!如果用户希望每次都有反馈(如游戏键盘输入),应慎用防抖
“取消函数只能靠 clearTimeout”对!但要确保保存了定时器引用(如上面的timeoutId

七、性能测试建议(实际项目中可用)

你可以用如下方式简单测试两者差异:

const start = performance.now(); function testDebounce() { const d = debounce(() => {}, 100); for (let i = 0; i < 100; i++) { d(); } console.log('防抖耗时:', performance.now() - start); } function testThrottle() { const t = throttle(() => {}, 100); for (let i = 0; i < 100; i++) { t(); } console.log('节流耗时:', performance.now() - start); }

你会发现:

  • 防抖:最终只会执行一次(即使调用了100次)
  • 节流:大约每100ms执行一次,共约10次左右(取决于具体实现细节)

八、结语:何时选哪个?

场景推荐策略
输入框搜索、自动补全防抖(immediate=false)
实时语音识别、打字速度统计节流(leading=true, trailing=false)
滚动加载分页节流(leading=true, trailing=true)
表单字段校验防抖(immediate=true)
自动保存草稿防抖(immediate=true)
大量DOM操作(如拖拽)节流(leading=true)

最佳实践建议:

  • 明确需求:你想要的是“停止后才响应”还是“固定频率响应”?
  • 使用cancel()在组件销毁或页面离开时释放资源,避免内存泄漏。
  • 若需复用,推荐封装成工具函数或类,便于维护。

总结

今天我们不仅讲清楚了防抖和节流的本质区别,还给出了生产级源码实现,涵盖了:

  • 立即执行选项(immediate)
  • 取消功能(cancel)
  • 完整的类型提示和文档风格
  • 类封装形式便于管理多个任务

这些代码可以直接集成进你的项目中,无论是 Vue、React 还是原生 JS 应用都适用。

记住一句话:

“好的性能不是靠堆硬件,而是靠聪明地控制事件流。”

希望今天的分享对你有帮助!欢迎留言交流你的实战经验

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/7 17:07:38

实际项目开发应用--485通信

一、485通信波特率的选择 长距离485Modbus通信时&#xff0c;波特率设置“小点更好” ——核心原则是“优先保证通信稳定性&#xff0c;再兼顾效率”&#xff0c;高波特率会加剧信号衰减、抗干扰能力下降&#xff0c;反而容易出现丢包、误码&#xff1b;低波特率虽通信速度慢&a…

作者头像 李华
网站建设 2026/6/2 11:30:55

【JavaSE】十八、URL HTTP请求格式 常见报头 状态码 会话保持

文章目录Ⅰ. URLⅡ. 报文格式Ⅲ. HTTP 请求方法&#x1f4a5; GET 和 POST 的区别Ⅳ. HTTP 常见报头Ⅴ. HTTP 状态码Ⅵ. 会话保持一、Cookie二、Session三、两者区别四、理解 cookie、session、token 三者的区别Ⅰ. URL 统一资源描述定位符 URL&#xff08;Uniform Resource L…

作者头像 李华
网站建设 2026/6/10 2:43:34

车间每天报喜不报忧,直到真 OEE 摆上墙,谁都装不下去!

目录 一、车间数据造假&#xff0c;到底有多日常&#xff1f; 1. 停机时间“自动消失” 2. 产量“向上取整”&#xff0c;报废“向下取整” 3. 点检表天天签&#xff0c;谁也没看过 二、为什么大家宁愿造假&#xff0c;也不愿报真实&#xff1f; 1. 指标只考结果&#xf…

作者头像 李华
网站建设 2026/6/9 20:59:06

python3.7-python3.12通过whl安装dlib

1、安装Cmakepip install cmake2、安装boostpip install cmake3、通过whl文件安装dlib下载链接中包括python3.7-python3.12版本对应的dlib库例如我的python版本是3.12&#xff0c;在.whl下载路径下&#xff0c;输入以下指令安装pip install dlib-19.24.2-cp312-cp312-win_amd64…

作者头像 李华
网站建设 2026/6/8 0:36:12

合并区间(二维vector使用,多维vector使用默认sort)

注意点&#xff1a; 1.sort自带的比较函数是支持多维数组比较的&#xff0c;使用的是字典序比较&#xff1b; 2.对于多维的vector&#xff0c;可以使用back&#xff0c;front,at等函数 比较例子&#xff1a; 二维 vector 示例 vector<vector<int>> v {{2,5},{1,3}…

作者头像 李华
网站建设 2026/6/5 13:29:55

ubuntu远程rdp连接屏幕分辨率太小

# 切换root权限 sudo -i # 编辑XRDP的会话配置文件 nano /etc/xrdp/startwm.sh在文件的最顶部&#xff08;#!/bin/sh下面&#xff09;添加一行分辨率配置&#xff08;比如设置为 1920x1080&#xff0c;可根据需求调整&#xff09;&#xff1a;bash运行# 设置XRDP默认分辨率&…

作者头像 李华