JS setTimeout & setInterval & requestAnimationFrame
· 6 min read
JavaScript 定时调度 三 大核心 API
- setTimeout:延迟执行一次,推荐使用,可灵活控制调度
- setInterval:周期重复执行,不推荐使用,存在设计缺陷
- requestAnimationFrame:与屏幕刷新同步,动画场景首选
最佳实践:用递归 setTimeout 替代 setInterval,保证间隔准确、支持动态调整。
零延迟调度:setTimeout(fn, 0) 不是立即执行,而是当前同步任务完成后尽快执行,可用于拆分长任务。
关键提醒:定时器回调中的 this 指向全局对象;务必清理不再需要的定时器防止内存泄漏。
setTimeout 基础
基本语法
const timerId = setTimeout(callback, delay, ...args);
callback:延迟执行的函数delay:延迟毫秒数(默认 0)...args:传递给回调的参数
Live Editor
function Demo() { function handleClick() { setTimeout(() => { alert('3 秒到了!'); }, 3000); } return <button onClick={handleClick}>3 秒后弹出提示</button>; }
Result
Loading...
取消调度
clearTimeout(timerId);
Live Editor
function Demo() { const [timerId, setTimerId] = useState(null); function start() { const id = setTimeout(() => { alert('你取消了,我不会出现'); }, 5000); setTimerId(id); } function cancel() { clearTimeout(timerId); alert('已取消定时器'); } return ( <> <button onClick={start}>5 秒后弹出</button> <button onClick={cancel} style={{marginLeft: '10px'}}>取消</button> </> ); }
Result
Loading...
setInterval 基础
基本语法
const timerId = setInterval(callback, delay, ...args);
停止周期执行
clearInterval(timerId);
Live Editor
function Demo() { const [count, setCount] = useState(0); const [timerId, setTimerId] = useState(null); function start() { const id = setInterval(() => { setCount(c => c + 1); }, 1000); setTimerId(id); } function stop() { clearInterval(timerId); } return ( <> <div>计时器: {count} 秒</div> <button onClick={start}>开始</button> <button onClick={stop} style={{marginLeft: '10px'}}>停止</button> </> ); }
Result
Loading...
caution
技术上 clearTimeout 和 clearInterval 可以互换,但永远不要混用!
setInterval 的致命缺陷
问题:时间间隔不准确
如果回调执行时间超过设定的 delay,两次执行之间没有间隔!
预期: |-----|-----|-----|
实际: |========|========|
执行 执行
问题:任务积压
如果页面卡顿或切换到后台,回调会被挂起,恢复后一次性执行所有积压任务。
解决方案:递归 setTimeout
原理
在上一次执行完成后,才调度下一次。
function tick() {
console.log('执行任务');
setTimeout(tick, 1000); // 完成后再调度下一次
}
setTimeout(tick, 1000);
优势
- 间隔准确:两次执行开始之间的间隔固定
- 灵活可调:可根据上一次执行结果动态调整延迟
- 无积压:任务不会堆积
动态调整延迟示例
let delay = 1000;
function fetchData() {
fetch('/api/data')
.then(res => res.json())
.then(data => {
if (data.load > 80) {
delay = 3000; // 服务器压力大,降低请求频率
}
setTimeout(fetchData, delay);
});
}
setTimeout(fetchData, delay);
tip
记住:永远优先使用递归 setTimeout 替代 setInterval!
setTimeout(fn, 0) 的真正含义
不是"立即执行",而是"尽快执行"
setTimeout(() => console.log('B'), 0);
console.log('A');
// 输出: A, B
事件循环中的位置
同步任务 → 微任务 → 渲染 → 宏任务(setTimeout)
应用场景 1:拆分长任务
// ❌ 阻塞 UI 1 秒
for (let i = 0; i < 1000000000; i++) {
heavyCalculation();
}
// ✅ 分批次执行,不阻塞 UI
let i = 0;
const CHUNK = 10000;
function process() {
const end = Math.min(i + CHUNK, 1000000000);
while (i < end) {
heavyCalculation();
i++;
}
if (i < 1000000000) {
setTimeout(process, 0);
}
}
process();
应用场景 2:让浏览器先渲染
// ❌ 用户看不到"计算中..."提示
button.onclick = function() {
status.textContent = '计算中...';
heavyCalculation(); // 阻塞 3 秒
status.textContent = '完成';
};
// ✅ 用户能看到提示
button.onclick = function() {
status.textContent = '计算中...';
setTimeout(() => {
heavyCalculation();
status.textContent = '完成';
}, 0);
};
requestAnimationFrame
为什么需要它?
setTimeout/setInterval 不与屏幕刷新同步:
屏幕刷新: | | | | |
定时器: | | | | | |
导致动画不流畅、掉帧、耗电。
语法
const id = requestAnimationFrame(callback);
cancelAnimationFrame(id);
优势
- 与屏幕刷新频率同步(通常 60fps = 每 16.7ms 一次)
- 页面后台时自动暂停,节省 CPU
- 浏览器内部优化,动画更流畅
Live Editor
function SmoothAnimation() { const [x, setX] = useState(0); const [animId, setAnimId] = useState(null); function animate() { setX(prev => { if (prev >= 300) { cancelAnimationFrame(animId); return 300; } return prev + 2; }); setAnimId(requestAnimationFrame(animate)); } return ( <> <button onClick={animate}>开始平滑动画</button> <div style={{ width: '50px', height: '50px', background: 'var(--ifm-color-primary)', borderRadius: '8px', transform: `translateX(${x}px)`, marginTop: '10px' }} /> </> ); }
Result
Loading...
进阶细节
最小延迟限制
浏览器对嵌套超过 5 层的 setTimeout 强制 4ms 最小延迟:
Live Editor
function Demo() { const [delays, setDelays] = useState([]); function test() { const times = []; let prev = Date.now(); let start = prev; function tick() { const now = Date.now(); times.push(now - prev); prev = now; if (now - start < 100) { setTimeout(tick, 0); } else { setDelays(times.join(', ')); } } setTimeout(tick, 0); } return ( <> <button onClick={test}>测试嵌套延迟</button> <div style={{marginTop: '10px'}}>每次调用间隔 (ms): {delays}</div> </> ); }
Result
Loading...
观察:前 4 次 < 4ms,之后 ≥ 4ms。
this 指向问题
const obj = {
value: 42,
print() {
console.log(this.value);
}
};
setTimeout(obj.print, 1000); // 输出 undefined,不是 42!
原因:setTimeout 调用时丢失了 this 上下文。
修复方案:
// 方案 1:箭头函数
setTimeout(() => obj.print(), 1000);
// 方案 2:bind
setTimeout(obj.print.bind(obj), 1000);
caution
即使在严格模式下,setTimeout 回调中的 this 仍然是 window(或 global),不是 undefined!
垃圾回收
定时器会持有回调函数的引用,防止被垃圾回收。
永远记得取消不再需要的定时器,否则会内存泄漏!
// ❌ 组件卸载后定时器仍在运行
useEffect(() => {
setInterval(() => {
console.log('每秒执行');
}, 1000);
}, []);
// ✅ 正确:清理函数
useEffect(() => {
const timer = setInterval(() => {
console.log('每秒执行');
}, 1000);
return () => clearInterval(timer); // 组件卸载时清除
}, []);
总结
| 特性 | setTimeout | setInterval | requestAnimationFrame |
|---|---|---|---|
| 执行次数 | 一次 | 多次 | 每次调度一次 |
| 精度 | 受 4ms 限制 | 受函数执行时间影响 | 与刷新同步 |
| 后台运行 | 降速 | 降速 | 暂停 |
| 推荐场景 | 延迟执行、轮询 | 不推荐 | 动画、视觉更新 |
核心建议:
- 需要动画 → 用 requestAnimationFrame
- 需要周期执行 → 用 递归 setTimeout
- setInterval 几乎没有合理的使用场景