先说结论:这个问题没有一个能一句话拍死的答案,要看你问的是浏览器里的 setTimeout,还是 Node.js 里的 setTimeout,更要看宿主的底层实现选了什么尺子。现代浏览器(Chrome、Edge、Firefox 的现行版本)里,定时器的内部调度更偏向“单调时钟”,也就是系统上电以来累加的 ticks;而 Node.js 在 Linux、macOS 上,定时器底层借用的 libuv 时钟并不回避系统墙钟,你真把系统时间拨一下,队列里已注册的 setTimeout 会明显给你脸色看。
我这么说不是凭空猜测。写 JavaScrip 这些年,几乎每个项目都会拿 setTimeout 做延迟、动画节流、轮询补料、失败重试。大多数时候它安静可靠,可一旦牵扯到 NTP 校时、虚拟机快照回滚、手动改系统时间这类场景,很多人就开始含糊:它到底是“过多久执行”,还是“到某个墙上绝对时间点才执行”?这篇博客就把这个问题拆开聊清楚,内容包括定时器在事件循环里的位置、浏览器与 Node.js 各自动用哪把时钟、我实测定时器受系统时间影响的复现过程,以及最后怎么绕开这些不确定性。适合所有写过 setTimeout 但还没深入到底层时钟机制的开发者阅读。
1. 先搞清楚“定时”这件事的底层逻辑
很多人把 setTimeout 想象成一个独立的小闹钟:你定好 5000 毫秒,系统到点就“叮”一声叫醒回调。真实情况完全不是这样。JavaScript 是单线程语言,同一时刻只有一个任务在执行,所谓“到点触发”只是把回调排进待执行队列的队尾,如果前面有长任务占着线程,定时器照样等。
1.1 setTimeout 回调不是到点就执行
我用一句话概括过:setTimeout 的 delay 是“最早能执行的时间”,不是“必然执行的时间”。它注册的瞬间,宿主环境会记录一个预计的执行时刻,也就是当前时间 + delay。随后事件循环开始循环,每次循环到一个 timer 阶段或者每过一小段时间,就看看当前时间是不是已经到了那个预计时刻。一旦到达,就把回调作为宏任务塞进队列;但塞进队列不等于立刻执行,执行还要等当前调用栈清空、前面的任务挨个处理完。
生活里最接近的类比是闹钟:你定了早上七点,闹钟在七点整尝试叫醒你,但你正在洗手间没听到,真正从床上爬起来可能是七点十分。浏览器里的定时器更夸张,因为它和渲染、事件处理、网络回调共享一个主线程。假设用户在主线程上跑了一个超大 for 循环占用了 3 秒,哪怕定时器只要 100 毫秒后触发,也只能等这 3 秒跑完才能轮到它。
这也是很多新手把“页面卡了导致 setTimeout 不准”误判成“定时器机制坏了”的原因。看底层实现之前,先得接受一个前提:定时器的粒度从来不是精确的,所以后续所有实验,我们关心的不是“误差几毫秒”,而是“系统时间被拨动后,会不会出现几分钟甚至几小时的偏移”。
1.2 系统时钟和经过时间,本质上是两把不同的尺子
要回答标题里的问题,必须先把两个概念掰开。第一个是墙钟时间(wall-clock time),也就是 Date.now()、new Date() 返回的那个时间戳。它描述的是“现在是几点几分”,能回答“我是不是已经到下午三点”这种绝对时刻问题。墙钟时间由系统维护,通常靠 NTP 协议与互联网时间服务器校准,也可能被用户手动调整,偶尔还会因为闰秒、时区设置出现前后跳变。
第二个是单调时钟(monotonic clock),它不关心现在是几点,只记录从某个固定起点开始累计走了多少 tick。这类时钟最重要的特性是只增不减,不会因为有人改了系统时间就回拨。在浏览器里 performance.now() 就是典型代表,在 Node.js 里 process.hrtime() 也是。它能回答的是“从 A 到 B 到底经过了多久”。
setTimeout 的语义本意显然是“经过 delay 毫秒后执行”,也就是听单调时钟的话。但“进行实现”并不是这样简单:宿主拿到一个 delay 后,需要把它换算成一个内部 deadline 存起来。如果底层用墙钟算 deadline,那么 deadline 就是一个绝对的墙上时刻,系统时间一变,它自然跟着遭殃;如果底层用单调时钟算 deadline,那么即使你把系统时间从 2024 年拨到 1999 年,已经注册的定时器也不会提前醒。
结论先放在这:语言规范没有规定死必须用哪把尺子,于是浏览器和 Node.js 各自选了不同路线。这就是问题没有唯一答案的根本原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 谁在底层“叫醒”setTimeout,它们又用了哪把尺子
了解了事件循环基本模型之后,我们把问题下沉到宿主实现层面。setTimeout 并不是 V8 引擎自己提供的 API,V8 只负责执行 JavaScript 代码,定时器、网络请求这类宿主能力是由外部环境注入的。浏览器里由 Blink/WebKit 这一层来管,Node.js 里则由 libuv 事件循环来管。两套体系对“时间”的理解完全不同,这正是问题最微妙的地方。
2.1 浏览器端:Chromium 的定时器更依赖单调时钟
拿使用最广的 Chromium 举例。Blink 里的 DOMTimer 在注册时,
