定时器(
setTimeout
/setInterval
)是 JavaScript 异步编程的核心工具,但开发者往往低估其复杂性。本文将揭示这两个 API 在高精度场景下的隐藏风险,并通过 代码示例展示如何构建健壮的定时调度系统。
JavaScript 是单线程语言,它只有一个主线程,所有任务都在这个线程上执行,这意味着一次只能做一件事。为了处理异步操作,比如计时器、网络请求等,JavaScript 使用了事件循环(Event Loop)机制。当一个异步操作完成时,它会被放入回调队列中等待主线程空闲时再执行。
一、setTimeout 和 setInterval 函数:
功能对比
setTimeout
:用来设定一个计时器,在指定的时间(毫秒)后仅执行一次给定的回调函数。setInterval
:类似于setTimeout
,但它会在每隔指定的时间间隔重复执行给定的函数,适用于需要定期执行的任务。
参数结构
两者都接受相似的参数:
- callback:当计时器到期时要执行的函数。
- time:对于
setTimeout
是延迟时间,对于setInterval
是每次执行之间的间隔时间,以毫秒为单位。非数字值会被视为0
,负数也会被当作0
处理。 - [arg1, arg2, ...] :可选参数,这些参数将在调用回调函数时传递给它。
返回值
setTimeout
和setInterval
都返回一个定时器 ID,这是一个非零整数值,代表已创建的计时器。这个返回值一般用于取消对应的定时器
那么,我们可以得出一个结论:
setTimeout一定会在指定时间后执行吗?
答案当然是否定的!为什么呢?
因为当你的主线程程序是无限次的循环时,那么主线程没有结束,就不会去执行定时器中的回调函数了。
那下面我们们来看看他们潜在的陷阱:
二、setInterval 的致命陷阱
2.1 回调堆积场景模拟
let executionCount = 0;
setInterval(() => {
const start = Date.now();
executionCount++;
// 模拟耗时操作
while(Date.now() - start < 150) {}
console.log(`Execution ${executionCount}: ${Date.now() - start}ms`);
}, 100);
/* 输出:
Execution 1: 150ms
Execution 2: 150ms (立即执行)
Execution 3: 150ms (立即执行)
*/
上图例子,当回调执行时间(150ms)超过间隔时间(100ms)时,后续回调会连续执行,导致:
- CPU 使用率飙升
- 内存泄漏风险增加
- 事件循环被阻塞
2.2 执行时序可视化
时间轴(ms) | 事件 | 队列状态 |
---|---|---|
0 | 设置定时器 | [回调1] |
100 | 尝试触发回调2 | [回调1, 回调2] |
150 | 完成回调1 | [回调2] |
200 | 尝试触发回调3 | [回调2, 回调3] |
300 | 完成回调2 | [回调3] |
三、setTimeout 的误差累积效应
3.1 误差放大实验
const startTime = new Date().getTime(), delay = 1000
let count = 0
let timer = setTimeout(doFunc, delay)
function doFunc(){
count++
console.log(new Date().getTime() - (startTime + count * 1000) + 'ms')
if(count < 10){
timer = setTimeout(doFunc, delay)
}
}
3.2:setTimeout 的最短延迟时间
因为如果设置的 timeout 小于 0,则设置为 0,如果嵌套的层级超过了 5 层(计时器嵌套),并且 timeout 小于 4ms,则设置 timeout 为 4ms。并且,在不同浏览器中出现这种最小延迟的情况有所不同
那么有没有什么好的处理方案呢?
四、完整实现方案:
- 管理器集成
- 使用
Map
结构全局管理定时器实例 - 支持通过唯一ID进行精准控制
- 自动垃圾回收机制防止内存泄漏
// 全局定时器管理中心
const timerManager = new Map<string, { stop: () => void }>();
function safeScheduler(callback: () => void, delay: number, id?: string) {
let expected = Date.now() + delay;
let timerId: ReturnType<typeof setTimeout>;
const controller = {
stop: () => {
clearTimeout(timerId);
if (id) timerManager.delete(id);
}
};
const timeoutHandler = () => {
const drift = Date.now() - expected;
callback();
expected += delay;
timerId = setTimeout(timeoutHandler, Math.max(0, delay - drift));
};
timerId = setTimeout(timeoutHandler, delay);
if (id) timerManager.set(id, controller);
return controller;
}
// 全局清除方法
function clearSafeScheduler(id: string) {
const timer = timerManager.get(id);
timer?.stop();
}
// 清除所有定时器
function clearAllSchedulers() {
timerManager.forEach(timer => timer.stop());
timerManager.clear();
}
调用方法:
// 启动带标识的定时器
const searchTimer = safeScheduler(
() => console.log('Search refresh'),
1000,
'search-api'
);
// 通过ID清除单个定时器
clearSafeScheduler('search-api');
// 紧急停止所有定时器
clearAllSchedulers();
vue组件集成:
// 在组件中
export default defineComponent({
mounted() {
this.autoRefresh = safeScheduler(this.fetchData, 5000, 'comp-refresh');
},
beforeUnmount() {
clearSafeScheduler('comp-refresh');
}
})