应使用 requestanimationframe:它不丢帧、自动暂停后台标签页、天然对齐浏览器刷新节奏;throttle 易导致掉帧,内联写法失效,passive 不能替代节流,intersectionobserver 更优。

直接用 requestAnimationFrame 配合标志位,比手写定时器版或时间戳版 throttle 更可靠——它不丢帧、自动暂停后台标签页、天然对齐浏览器刷新节奏。
scroll 事件该用 requestAnimationFrame 还是 throttle?
用 requestAnimationFrame。不是“可以”,而是“应该”:浏览器每帧约 16ms 刷新一次,requestAnimationFrame 自动卡在这个节奏上;而 throttle(handleScroll, 100) 这类定时器节流,若内部逻辑耗时超过 16ms,就会挤占渲染时间,导致掉帧。
- 高频滚动中,
throttle仍可能每秒强制执行多次(比如设为 50ms),但实际不需要这么密 -
requestAnimationFrame在标签页切到后台时自动暂停回调,省 CPU;setTimeout版节流不会 - 必须加标志位防止重复注册帧回调,否则快速滚动时会堆积多个
requestAnimationFrame调用
正确写法:
let ticking = false;
function handleScroll() {
if (!ticking) {
requestAnimationFrame(() => {
updatePosition(); // 实际逻辑
ticking = false;
});
ticking = true;
}
}
window.addEventListener('scroll', handleScroll);
为什么不能在 HTML 里写 onscroll="throttle(...)"?
因为内联事件处理器(如 onscroll 属性)每次触发都重新解析字符串,无法维持闭包变量——throttle 依赖的 lastTime 或 timeout 状态全丢了,等于没节流。
-
throttle函数返回的是一个新函数,需通过addEventListener绑定才能复用闭包环境 - 内联写法还会丢失
this和事件参数,event对象拿不到 - 即使把
throttle挂到window上,也违背模块封装原则,且移动端 iOS Safari 旧版本对passive支持不全,更需统一控制入口
throttle 的时间戳版和定时器版有什么实际区别?
行为完全不同,选错会导致首屏响应延迟或末次动作被忽略。
- 时间戳版(如 Lodash 默认):首次触发立即执行,之后按间隔放行 —— 适合
scroll、mousemove这类“动起来就要反馈”的场景 - 定时器版:首次触发后延迟执行,后续触发只重置定时器 —— 实际更接近防抖,适合按钮提交等“宁可晚点也不能漏”的操作
- 自己手写时别混用逻辑;如果真要用纯
throttle,推荐时间戳版,并显式处理this和arguments,否则getBoundingClientRect调用会报错
简易时间戳版示例:
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
加了 { passive: true } 就不用节流了吗?
完全不够。它只解决「浏览器等待 JS 执行完才滚动」的问题,不减少调用次数。
- 加了
{ passive: true }后,preventDefault()失效,滚动变顺滑,但handleScroll依然每像素都触发 - CPU 占用照样飙升,尤其在复杂 DOM 或低配设备上
- 必须搭配
requestAnimationFrame或节流使用;iOS Safari 12 以下不支持passive,得做特性检测降级
安全写法:
const options = 'ontouchstart' in window ? { passive: true } : false;
window.addEventListener('scroll', handleScroll, options);
真正容易被忽略的是:视口检测这类需求,根本不用监听 scroll + getBoundingClientRect 手算,直接上 IntersectionObserver——它由浏览器原生实现,性能零成本,还自动处理元素销毁、iframe 边界等边界情况。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











