如何利用闭包技术高效实现滚动事件节流函数,控制频率?
- 内容介绍
- 相关推荐
本文共计883个文字,预计阅读时间需要4分钟。
要使用闭包构建高性能节流函数来控制滚动事件频率,关键在于封装状态变量(如上一次执行时间或定时器ID)。以下是简化的实现:
为什么必须用闭包
节流需要记住两个核心状态:
- lastTime:记录上一次真正执行回调的时间戳,用于判断是否过了间隔期
-
timer:保存
setTimeout返回的 ID,用于清除待执行的延迟任务
这两个值不能每次调用都重置,也不能暴露给外部。只有闭包能让它们长期驻留在函数作用域内——既不被外部访问,又能在多次事件触发中持续复用。不用闭包就得依赖全局变量,会导致多个滚动监听器互相覆盖、竞态出错。
选对版本:时间戳版 vs 定时器版
两种主流实现逻辑不同,适用场景也不同:
-
时间戳版:首次触发立刻执行,后续在
delay内的调用全部忽略,直到超过间隔才再次执行。适合scroll监听,确保“至少每 X 毫秒响应一次” -
定时器版:首次触发设定时器,
delay后执行;期间重复触发会不断清除并重设,最终只执行最后一次。适合“松手后补一次”的交互,比如滚动到底部加载更多
混用两者逻辑(比如在时间戳版里加 clearTimeout)会导致状态混乱、漏执行或重复执行。
带 leading/trailing 控制的实用写法
真实业务中常需要更精细的控制:
-
leading: true → 首次触发立即执行,并更新
lastTime -
trailing: true → 每次触发都尝试设新定时器;若已有
timer就先清除,再设新的,确保只留最后一次 - trailing 回调里要重新检查:
if (now - lastTime >= delay),否则可能和 leading 重复执行
示例(时间戳 + leading 支持):
function throttle(func, delay, options = {}) {const { leading = true, trailing = false } = options;
let lastTime = 0;
let timer = null;
return function(...args) {
const now = Date.now();
const context = this;
// 首次立即执行
if (leading && !lastTime) {
func.apply(context, args);
lastTime = now;
}
// 时间戳主逻辑:够间隔才执行
if (now - lastTime >= delay) {
func.apply(context, args);
lastTime = now;
} else if (trailing && !timer) {
// 补尾:设一个延迟执行,确保最后一次有机会触发
timer = setTimeout(() => {
if (now - lastTime >= delay) {
func.apply(context, args);
lastTime = Date.now();
}
timer = null;
}, delay - (now - lastTime));
}
};
}
绑定到滚动事件的正确姿势
直接写在 addEventListener 里容易丢失 this 或参数,推荐这样用:
console.log('滚动位置:', window.scrollY);
// 执行懒加载、吸顶等逻辑
}, 100, { leading: true, trailing: true });
window.addEventListener('scroll', handleScroll);
注意:现代浏览器支持 { passive: true } 提升滚动性能,但若节流函数里需要调用 preventDefault()(如阻止默认滚动),则不能设为 passive。
本文共计883个文字,预计阅读时间需要4分钟。
要使用闭包构建高性能节流函数来控制滚动事件频率,关键在于封装状态变量(如上一次执行时间或定时器ID)。以下是简化的实现:
为什么必须用闭包
节流需要记住两个核心状态:
- lastTime:记录上一次真正执行回调的时间戳,用于判断是否过了间隔期
-
timer:保存
setTimeout返回的 ID,用于清除待执行的延迟任务
这两个值不能每次调用都重置,也不能暴露给外部。只有闭包能让它们长期驻留在函数作用域内——既不被外部访问,又能在多次事件触发中持续复用。不用闭包就得依赖全局变量,会导致多个滚动监听器互相覆盖、竞态出错。
选对版本:时间戳版 vs 定时器版
两种主流实现逻辑不同,适用场景也不同:
-
时间戳版:首次触发立刻执行,后续在
delay内的调用全部忽略,直到超过间隔才再次执行。适合scroll监听,确保“至少每 X 毫秒响应一次” -
定时器版:首次触发设定时器,
delay后执行;期间重复触发会不断清除并重设,最终只执行最后一次。适合“松手后补一次”的交互,比如滚动到底部加载更多
混用两者逻辑(比如在时间戳版里加 clearTimeout)会导致状态混乱、漏执行或重复执行。
带 leading/trailing 控制的实用写法
真实业务中常需要更精细的控制:
-
leading: true → 首次触发立即执行,并更新
lastTime -
trailing: true → 每次触发都尝试设新定时器;若已有
timer就先清除,再设新的,确保只留最后一次 - trailing 回调里要重新检查:
if (now - lastTime >= delay),否则可能和 leading 重复执行
示例(时间戳 + leading 支持):
function throttle(func, delay, options = {}) {const { leading = true, trailing = false } = options;
let lastTime = 0;
let timer = null;
return function(...args) {
const now = Date.now();
const context = this;
// 首次立即执行
if (leading && !lastTime) {
func.apply(context, args);
lastTime = now;
}
// 时间戳主逻辑:够间隔才执行
if (now - lastTime >= delay) {
func.apply(context, args);
lastTime = now;
} else if (trailing && !timer) {
// 补尾:设一个延迟执行,确保最后一次有机会触发
timer = setTimeout(() => {
if (now - lastTime >= delay) {
func.apply(context, args);
lastTime = Date.now();
}
timer = null;
}, delay - (now - lastTime));
}
};
}
绑定到滚动事件的正确姿势
直接写在 addEventListener 里容易丢失 this 或参数,推荐这样用:
console.log('滚动位置:', window.scrollY);
// 执行懒加载、吸顶等逻辑
}, 100, { leading: true, trailing: true });
window.addEventListener('scroll', handleScroll);
注意:现代浏览器支持 { passive: true } 提升滚动性能,但若节流函数里需要调用 preventDefault()(如阻止默认滚动),则不能设为 passive。

