JavaScript 定时器工作原理与事件循环机制解析
本文深入解析 JavaScript 中 setTimeout 和 setInterval 的工作机制,重点阐述事件循环如何影响回调函数的执行时机。通过对比两种定时器在处理耗时任务时的行为差异,揭示回调函数加入事件队列的具体规则,帮助开发者避免因执行阻塞导致的定时器行为偏差。
setTimeout 基本语法与用法
setTimeout()函数
MDN对 setTimeout 的官方定义如下:
该函数在设定的延迟时间结束后,执行指定的代码片段或调用函数。
语法格式
setTimeout 的调用语法简单,首参数接收回调函数,次参数指定延迟时长。该函数返回一个数值类型的唯一标识符(ID),此 ID 可作为 clearTimeout 的参数以取消定时器:
var timeoutID = window.setTimeout(code, delay);
IE0+ 浏览器还支持向回调函数传递额外参数:
var timeoutID = window.setTimeout(func, delay, [param1, param2, ...]);
setInterval 与 setTimeout 的区别
setInterval()函数
MDN 对 setInterval 的定义为:
周期性地调用一个函数(function)或者执行一段代码。
鉴于 setInterval 与 setTimeout 的使用方式相似,此处不再赘述。
延迟参数与事件队列机制
关于第二个参数(delay)的详细说明
受 JavaScript 事件循环机制影响,第二个参数并不保证在延迟 delay 毫秒后立即执行回调函数,而是尝试将回调函数加入事件队列。实际上,setTimeout 和 setInterval 在此处理上存在差异:
- setTimeout:在延时 delay 毫秒后,无论其他情况如何,直接将回调函数加入事件队列。
- setInterval:在延时 delay 毫秒后,先检查事件队列中是否已存在未执行的回调函数(即 setInterval 自身的回调),若存在,则不再向队列添加新的回调函数。
因此,当代码中存在耗时任务时,定时器的表现往往与预期不符。
耗时任务对 setTimeout 的影响
通过具体示例进行解析
以下代码旨在分别在 100ms 和 200ms 时(即等待 100ms 后)调用回调函数:
var timerStart1 = now();
setTimeout(function () {
console.log('第一个setTimeout回调执行等待时间:', now() - timerStart1);
var timerStart2 = now();
setTimeout(function () {
console.log('第二个setTimeout回调执行等待时间:', now() - timerStart2);
}, 100);
}, 100);
// 输出:
// 第一个setTimeout回调执行等待时间: 106
// 第二个setTimeout回调执行等待时间: 107
若无其他干扰,结果看似符合预期;但一旦代码中包含耗时任务,结果便偏离预期:
var timerStart1 = now();
setTimeout(function () {
console.log('第一个setTimeout回调执行等待时间:', now() - timerStart1);
var timerStart2 = now();
setTimeout(function () {
console.log('第二个setTimeout回调执行等待时间:', now() - timerStart2);
}, 100);
heavyTask(); // 耗时任务
}, 100);
var loopStart = now();
heavyTask(); // 耗时任务
console.log('heavyTask耗费时间:', now() - loopStart);
function heavyTask() {
var s = now();
while(now() - s < 1000) {
}
}
function now () {
return new Date();
}
// 输出:
// heavyTask耗费时间: 1015
// 第一个setTimeout回调执行等待时间: 1018
// 第二个setTimeout回调执行等待时间: 1000
由于耗时任务的存在,两个 setTimeout 的实际等待时间均不再是 100ms!具体过程如下:
- 首先,第一个耗时任务(heavyTask())开始执行,其运行时间约为 1000ms。
- 耗时任务启动 100ms 后,第一个 setTimeout 的回调函数被期望执行,因此被加入事件队列。但由于前序耗时任务尚未结束,该回调只能在队列中等待,直至耗时任务执行完毕后才开始执行。因此,实际观察到的第一个 setTimeout 回调执行等待时间为 1018ms。
- 第一个 setTimeout 回调执行完毕后,立即启动了第二个 setTimeout,该定时器同样期望在延时 100ms 后执行回调。然而,由于第一个 setTimeout 内部存在耗时任务,其执行逻辑与第一个定时器类似,同样等待了约 1000ms 后才开始执行。
可用下图进行概括:

setInterval 的队列检查机制
再来看 setInterval 的一个示例:
var intervalStart = now();
setInterval(function () {
console.log('interval距定义定时器的时间:', now() - loopStart);
}, 100);
var loopStart = now();
heavyTask();
console.log('heavyTask耗费时间:', now() - loopStart);
function heavyTask() {
var s = now();
while(now() - s < 1000) {
}
}
function now () {
return new Date();
}
// 输出:
// heavyTask耗费时间: 1013
// interval距定义定时器的时间: 1016
// interval距定义定时器的时间: 1123
// interval距定义定时器的时间: 1224
上述代码期望每隔 100ms 输出一条日志。与 setTimeout 不同,setInterval 在尝试将回调函数加入事件队列前,会判断队列中是否还有未执行的回调;若有,则不再添加,以避免多个回调同时执行的情况。
可用下图进行概括:

总结
总结
上文对 JavaScript 定时器的执行原理进行了简要分析,旨在帮助读者更深入地理解 JavaScript。文中若有描述不当之处,欢迎在评论区指正。
网页开发三者分工:HTML结构、CSS样式与JavaScript交互怎么协作
2026-11-05 12:51:22
JS解析URL参数时怎么处理中文乱码和特殊字符
2026-11-05 12:47:23
js-cookie 存储 JSON 对象:自动序列化与反序列化技巧
2026-11-05 12:36:26
marked.js 快速上手:安装与基础 Markdown 转 HTML 示例
2026-11-05 09:32:53
Phaser.js 与 PixiJS、Cocos Creator 的技术选型对比
2026-11-05 08:36:30
