在前端开发中,我们经常会遇到这种场景:用户疯狂点击“点赞”按钮,或者鼠标疯狂滚动页面。如果不做限制,后台接口可能会被瞬间打爆,浏览器也会卡死。
解决这个问题的标准答案是节流(Throttle)。而在实现节流时,最优雅、最经典的手段就是闭包(Closure)。
今天,我们用大白话把这俩概念彻底说透。
一、 什么是节流?一句话:技能冷却
打过游戏的都知道,法师放完一个大招后,技能图标会变成灰色(进入 CD 时间)。在这个时间内,你按烂键盘也放不出第二个大招。必须等 CD 转好,才能再次释放。
节流就是给函数加上“技能冷却”。 在指定的时间内,无论用户触发了多少次事件,函数只执行一次。
二、 最经典的节流代码(一看就懂)
直接上代码,这是面试时闭着眼睛都要能写出来的标准模板:
function throttle(fn, delay) {
// 1. 定义一个“锁”(定时器)
let timer = null;
// 2. 返回一个真正会被事件触发的内部函数
return function(...args) {
// 3. 如果锁还在(技能CD中),直接拦截,什么都不做
if (timer) return;
// 4. 如果没锁,说明可以释放技能!立即上锁,并开始倒计时
timer = setTimeout(() => {
fn.apply(this, args); // 真正执行业务逻辑(比如点赞、加载数据)
timer = null; // 倒计时结束,开锁(CD转好了)
}, delay);
}
}
// 实际使用:给滚动事件加上 300 毫秒的冷却时间
window.onscroll = throttle(function() {
console.log('页面滚动了!');
}, 300);
三、 为什么这叫闭包?闭包到底干了啥?
很多新手看着上面的代码会发懵:这怎么就闭包了?
闭包的本质就是:内部函数访问了外部函数的变量,并且让这个变量活了下来。
结合上面的代码看:
timer是在外层throttle函数里定义的。- 里层
return出来的那个匿名函数,偷偷使用了外层的timer。 - 重点来了:外层函数
throttle执行完就结束了,按理说timer应该被垃圾回收机制清理掉。但是!里层的函数被绑定到了window.onscroll上,它要长期存活。因为里层函数一直抓着timer不放,导致timer也被迫长期活在内存里。
这就是闭包。 timer 就像被里层函数“关”起来的一个私有变量。
四、 灵魂拷问:不写闭包行不行?
面试官经常会问:“我知道闭包能存状态,那我不用闭包,直接搞个全局变量存 timer 行不行?”
代码大概长这样:
let globalTimer = null; // 全局变量当锁
function badThrottle(fn, delay) {
if (globalTimer) return;
globalTimer = setTimeout(() => {
fn();
globalTimer = null;
}, delay);
}
答案是:极其难用。 这种写法有两个致命缺陷:
- 全局污染: 你的
globalTimer暴露在全局环境下,别的文件如果也有个同名变量,代码瞬间崩溃。甚至别人可以在控制台直接输入globalTimer = null来破解你的节流。 - 一锁锁全家(状态冲突): 这是最致命的。假设你的页面上有两个功能:滚动加载和点赞。如果它们共用这个全局
globalTimer,就会发生灾难——当你正在滚动页面(触发了定时器,上了锁)的时候,你去点击“点赞”,会发现点赞按钮失效了!因为锁已经被滚动事件抢走了。
五、 闭包的降维打击:完美的状态隔离
我们再看回闭包写法:
const scrollThrottle = throttle(loadMore, 1000);
const likeThrottle = throttle(like, 1000);
当我们调用两次 throttle 函数时,神奇的事情发生了:
每次调用,都会在内存中开辟一个全新、独立的闭包环境。
- 滚动事件拥有自己的
timerA。 - 点赞事件拥有自己的
timerB。
它们不仅都被完美地隐藏在作用域内部(解决了全局污染),而且互不干扰(解决了状态冲突)。你滑你的页面,我点我的赞,大家井水不犯河水。
六、 再补一个高频兄弟:防抖(Debounce)
节流是“技能冷却期间不响应”; 防抖是“等你彻底停下来再执行一次”。
比如搜索框输入联想:你每敲一个字都发请求会非常浪费。更合理的做法是,等用户停止输入一小段时间后,再发一次请求。
下面这段就是经典防抖写法,同样依赖闭包来保存 timer 状态:
function debounce(fn, delay) {
// 1. 定义在外部作用域的变量
let timer = null;
// 2. 返回内部函数
return function (...args) {
// 3. 内部函数访问了外部的 timer -> 形成闭包
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
一句话区分:
- 节流(Throttle):固定时间内最多执行一次。
- 防抖(Debounce):连续触发时不执行,停止触发后执行一次。
总结
节流里的 timer 不能每次执行时都重置,必须有一个安全的地方把它存起来。
全局变量太危险且容易冲突,而闭包完美地做到了“既隐藏了私有变量,又实现了状态的相互隔离”。
这就是前端大佬们不约而同选择闭包来实现节流的根本原因。
Leave a comment