从闭包到实战:彻底掌握节流函数的多种实现方式
要让节流函数真正实现“固定时间频率限制”,闭包并非可有可无的辅助手段,而是不可或缺的核心架构。它把 lastTime 和 timer 这类关键状态锁定在独立的作用域中,既不会被外部代码意外干扰,也不会与其他节流实例产生状态冲突。理解这一原理,才算抓住了节流设计的灵魂。

为什么闭包是封装状态的必然选择
节流绝不仅仅是简单地加一个 setTimeout 就能搞定。它需要两个核心状态持久存在:
- lastTime:记录上一次实际执行的时间戳,用于判定间隔时长是否满足要求
- timer:保存当前待执行的定时器ID,以便清除旧任务、防止重复触发
这两个值既不能在每次调用时重置,也不能放在全局作用域中——试想,两个滚动监听器如果共享同一个 lastTime,或者 resize 事件和 scroll 事件互相覆盖 timer,整个执行节奏就会完全失控。闭包的巧妙之处在于,每次执行 throttle(fn, 100) 时都会生成一个专属上下文,状态彼此隔离、互不干扰。
时间戳版本:保频型,适用于滚动、缩放等稳定采样场景
这个版本的逻辑非常直白:“首次立即执行,之后仅在间隔达标时放行”。它不依赖定时器,响应迅速,且没有延时偏差。
- 每次触发时,使用
Date.now()与lastTime进行差值判断 - 当
now - lastTime >= delay时,才执行回调并更新lastTime - 非常适合 scroll、resize、mousemove 等需要“至少每隔 X 毫秒响应一次”的场景
换句话说,时间戳版本保证的是执行频率——你无法跳过这个时间间隔,但首次触发的即时性非常出色。
定时器版本:收尾型,适合输入搜索、加载更多等场景
它的逻辑正好相反:“每次触发都重置延迟任务,最终只执行最后一次”。通过 clearTimeout 和 setTimeout 的协作来实现。
- 首次触发时设定定时器,delay 后执行回调
- 期间每次重复触发,先
clearTimeout(timer),再重新设定定时器 - 回调执行后必须将
timer = null,避免残留状态干扰下一轮 - 最适合输入框搜索、按钮防连点、滚动到底部加载更多等“松手后才响应”的需求
定时器版本关注的是“最后一次”,而不是“频率”——虽然你持续触发,但实际执行的只有停止触发的那一次。
带 leading/trailing 的增强版:首尾可控,仍依赖闭包统一管理
很多业务场景需要更精细的控制:拖拽时希望第一时间动起来(leading),松手后还要补一次加载(trailing)。这种需求靠拼凑逻辑难以实现,必须由一个统一的闭包来维护所有状态。
leading: true→ 首次调用立即执行,同时更新lastTimetrailing: true→ 每次调用都尝试设定定时器,但只保留最后一个- trailing 回调中还需二次判断:
if (Date.now() - lastTime >= delay),避免与 leading 冲突 timer、lastTime、pending标志位全部在同一个闭包内闭环流转
这才是闭包真正的价值所在——它让多个状态之间能够顺畅协同,而不是各自为政。
进阶建议:视觉场景优先使用 requestAnimationFrame
如果你的目标是让 DOM 更新更流畅(例如吸顶、视差滚动、懒加载),硬写 throttle(fn, 16) 不如直接拥抱 RAF 闭包。
- 闭包内维护
isQueued = false,确保同一帧只注册一次requestAnimationFrame - 滚动事件中只做数据采集(比如缓存 scrollTop),RAF 回调中再批量更新 DOM
- 天然对齐浏览器刷新节奏,比固定 delay 更稳定、更节省资源
一句话总结:节流的根基是闭包,但具体选用哪个版本、搭配什么策略,最终取决于业务场景的特性。选对了,事半功倍。
