setinterval 会“穿插”是因为它只按固定时间发起任务,不等待前次完成;递归 settimeout 则确保前次彻底结束才启动下一轮,避免并发与状态混乱。

用 setInterval 定时执行任务时,如果任务执行时间超过设定间隔,就会出现“执行穿插”——即上一次还没结束,下一次又触发了,导致并发、状态混乱甚至内存泄漏。根本解法不是调大间隔,而是改用递归 setTimeout,确保前一次任务彻底完成,才安排下一次。
为什么 setInterval 会“穿插”?
setInterval 只负责“按时发起”,不关心上次是否结束。比如设为 100ms,但某次任务耗时 150ms,那么第 2 次调用会在第 100ms 准时开始(此时第 1 次还在跑),第 3 次在第 200ms 开始……很快形成多个并行执行的实例。
常见于:轮询接口、动画帧控制、日志上报等实际耗时不可控的场景。
递归 setTimeout 的核心逻辑
每次任务执行完,再用 setTimeout 主动发起下一轮。这样“启动时机”由上一轮结束决定,天然规避重叠。
- 不是“固定间隔触发”,而是“完成之后再等 X ms”
- 任务体内部无需加锁或防重入,逻辑更干净
- 便于动态调整下次延迟(例如失败后退避重试)
写法示例与关键细节
基础结构:
setInterval(() => apiCall(), 2000);
✅ 正确写法:
function poll() {apiCall().then(() => {
setTimeout(poll, 2000); // 成功后才设下一次
}).catch(() => {
setTimeout(poll, 5000); // 失败延后重试
});
}
poll(); // 启动
注意点:
- 避免在任务中直接调用自身(如
poll()放在then外),否则会立即递归,栈溢出 - 需要手动清理时,只需清除当前 pending 的 timeout ID(用
clearTimeout) - 若需精确间隔(如动画对齐 requestAnimationFrame),可记录起始时间,动态计算补偿延迟
何时仍可用 setInterval?
仅适用于任务是纯同步、确定快于间隔的场景,例如:
- 更新毫秒级倒计时显示(DOM 操作极轻量)
- 心跳信号(空请求,超时短且服务端有幂等)
- 开发调试时快速验证逻辑(非生产环境)
一旦涉及异步、网络、DOM 计算或用户交互响应,优先选递归 setTimeout。










