JavaScript 调度机制完全指南:setTimeout 与 setInterval 的用法、精度与陷阱

发布时间:2026/10/7 2:08:19
JavaScript 调度机制完全指南:setTimeout 与 setInterval 的用法、精度与陷阱 文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载导读在编写 JavaScript 时我们常常需要延迟执行或周期性执行某段逻辑轮询后端接口、节流用户输入、展示倒计时、分割耗时的 CPU 任务等这些都依赖定时器调度。本指南以《现代 JavaScript 教程》zh.javascript.info中 调度setTimeout 和 setInterval 一章为主体系统讲解这两个内建调度方法的完整语法、取消调度、嵌套 setTimeout 与 setInterval 的精度差异、零延时调度以及浏览器环境的 4ms 嵌套限制并配合仓库中配套的课后任务与源码示例帮助你写出精确、可维护的定时器代码同时理解定时器在事件循环中的真实执行时机。什么是计划调用在 JavaScript 中我们并不总是希望函数被立即执行而是希望在特定的一段时间之后再执行这就是所谓的计划调用scheduling a call。目前有两种方式可以实现setTimeout允许我们将函数推迟到一段时间间隔之后再执行一次。setInterval允许我们重复运行一个函数从一段时间间隔之后开始运行之后以该时间间隔连续重复运行该函数。值得说明的是这两个方法并不在 JavaScript 语言规范ECMAScript中而是由运行环境提供的内建调度程序。目前所有主流浏览器以及 Node.js 都支持这两个方法在浏览器环境中它们的行为细节由 HTML5 标准的 timers 章节 描述而在 Node.js 中则由 timers 模块提供。setTimeout延迟执行一次语法与参数let timerId setTimeout(func|code, [delay], [arg1], [arg2], ...)各参数含义如下参数说明func\|code想要执行的函数或代码字符串。一般传入的都是函数由于历史原因也支持代码字符串但不建议使用delay执行前的延时以毫秒为单位1000 毫秒 1 秒默认值是0arg1arg2...要传入被执行函数或代码字符串的参数列表IE9 以下不支持基础用法函数在 1 秒后执行function sayHi() { alert(Hello); } setTimeout(sayHi, 1000);带参数的情况延时结束后传入实参function sayHi(phrase, who) { alert(phrase , who); } setTimeout(sayHi, 1000, Hello, John); // Hello, John如果第一个参数传入的是字符串JavaScript 会自动为其创建一个函数因此下面这种写法也能工作setTimeout(alert(Hello), 1000);但更推荐使用箭头函数替代字符串既避免了字符串求值的安全隐患也保持了代码的可读性setTimeout(() alert(Hello), 1000);常见错误传入了执行结果而非函数引用新手开发者常犯的一个错误是在函数名后面多加了一对括号// 错的 setTimeout(sayHi(), 1000);这样不行因为setTimeout期望得到一个函数的引用。而这里的sayHi()会立即执行函数实际上传入setTimeout的是函数的执行结果本例中为undefined。由于sayHi()没有返回任何值最终什么也没有被调度。用 clearTimeout 取消调度setTimeout在调用时会返回一个定时器标识符timer identifier即示例中的timerId我们可以使用它来取消这次调度let timerId setTimeout(...); clearTimeout(timerId);在下面的代码中我们对一个函数进行了调度紧接着又取消了这次调度所以最后什么也不会发生let timerId setTimeout(() alert(never happens), 1000); alert(timerId); // 定时器标识符 clearTimeout(timerId); alert(timerId); // 还是这个标识符并不会因为调度被取消而变成 null从alert的输出可以看到在浏览器中定时器标识符是一个数字在其他环境中可能是其他类型——例如 Node.js 返回的是一个定时器对象该对象包含一系列方法。由于这些方法没有统一的规范定义标识符的具体形态因环境而异这是正常现象。setInterval周期性重复执行setInterval的语法与setTimeout完全相同let timerId setInterval(func|code, [delay], [arg1], [arg2], ...)所有参数的意义相同区别在于setTimeout只执行一次而setInterval每间隔给定的时间周期性执行。想要阻止后续调用需要调用clearInterval(timerId)。下面的例子每隔 2 秒输出一条消息5 秒后停止// 每 2 秒重复一次 let timerId setInterval(() alert(tick), 2000); // 5 秒之后停止 setTimeout(() { clearInterval(timerId); alert(stop); }, 5000);注意alert 弹窗显示时计时器依然在计时在大多数浏览器中包括 Chrome 和 Firefox当显示alert/confirm/prompt弹窗时内部的定时器仍旧会继续嘀嗒。因此如果运行上述代码时没有及时关闭alert弹窗那么关闭弹窗后下一个alert会立即出现两次alert之间的实际间隔将小于 2 秒。嵌套的 setTimeout比 setInterval 更灵活、更精确周期性调度有两种方式一是直接使用setInterval二是使用嵌套的setTimeout——在回调函数内部再次调度自己let timerId setTimeout(function tick() { alert(tick); timerId setTimeout(tick, 2000); // (*) 本次执行完成后立即调度下一次 }, 2000);上面的setTimeout在当前这一次函数执行完时(*)处立即调度下一次调用。嵌套的setTimeout比setInterval灵活得多它可以根据当前执行的结果来决定下一次调度的间隔或行为。例如实现一个每 5 秒向服务器发送数据请求的服务当服务器过载时自动降低请求频率间隔翻倍let delay 5000; let timerId setTimeout(function request() { ...发送请求... if (request failed due to server overload) { // 下一次执行的间隔是当前的 2 倍 delay * 2; } timerId setTimeout(request, delay); }, delay);此外如果调度的函数占用大量 CPU我们还可以测量函数执行所花费的时间据此决定下一次调用应当提前还是推迟。为什么嵌套 setTimeout 比 setInterval 更精确嵌套的setTimeout相较于setInterval能够更精确地设置两次执行之间的延时。我们对比两个等价的代码片段。使用setIntervallet i 1; setInterval(function() { func(i); }, 100);使用嵌套的setTimeoutlet i 1; setTimeout(function run() { func(i); setTimeout(run, 100); }, 100);对setInterval而言内部的调度程序会每间隔 100 毫秒执行一次func(i)注意上图揭示的一个关键事实使用setInterval时func函数的实际调用间隔要比代码中设定的时间间隔短因为func自身的执行时间消耗掉了一部分间隔。如果func的执行时间超过了设定的 100 毫秒JavaScript 引擎会等待func执行完成然后检查调度程序若时间已到则立即再次执行它。极端情况下如果函数每次执行时间都超过delay那么各次调用之间将完全没有停顿。而嵌套的setTimeout则能确保延时的固定这里是 100 毫秒这是因为下一次调用是在前一次调用完成之后才被调度的两次执行之间的间隔严格等于设定的延时不会被函数自身的执行时间侵蚀。垃圾回收与定时器回调当一个函数传入setInterval/setTimeout时运行环境会为其创建一个内部引用并保存在调度程序中。这样即使该函数在其他地方没有任何引用垃圾回收器GC也不会将其回收// 在调度程序调用这个函数之前这个函数将一直存在于内存中 setTimeout(function() {...}, 100);对于setInterval传入的函数也会一直存在于内存中直到clearInterval被调用。这里还有一个副作用需要留意如果函数通过闭包引用了外部变量那么只要函数还存在这些外部变量也会随之存活它们可能比函数本身占用更多内存。因此当我们不再需要某个调度函数时最好主动取消它——即使它是一个占用内存很小的函数。零延时 setTimeout 与事件循环setTimeout(func, 0)或简写为setTimeout(func)是一种特殊用法它让func尽快执行但只有在当前正在执行的脚本执行完成后调度程序才会调用它。也就是说该函数被调度在当前脚本执行完成之后立即执行。setTimeout(() alert(World)); alert(Hello);第一行代码将调用安排到日程calendar0 毫秒处但调度程序只有在当前脚本执行完毕时才会去检查日程所以上面的输出顺序必然是先Hello然后才是World。这与事件循环的宏任务队列机制直接相关任何setTimeout回调都只会作为新的宏任务在当前代码执行完毕之后才被取出执行。仓库中 事件循环微任务和宏任务 一章对这种引擎等待任务 → 执行任务 → 休眠的循环有更详细的阐述其中明确指出当安排的scheduledsetTimeout时间到达时任务就是执行其回调并且引擎执行任务时永远不会进行渲染仅在任务完成后才会绘制对 DOM 的更改。浏览器中的零延时实际上不为零在浏览器环境下嵌套定时器的运行频率是受限制的。根据 HTML5 标准经过 5 重嵌套定时器之后时间间隔被强制设定为至少 4 毫秒。也就是说当定时器调用嵌套超过 5 层时最小延时会被强制钳制为 4ms。用下面的示例来观察这一现象。其中setTimeout以零延时不断重新调度自身每次调用都把实际时间记录到times数组let start Date.now(); let times []; setTimeout(function run() { times.push(Date.now() - start); // 保存前一个调用的延时 if (start 100 Date.now()) alert(times); // 100 毫秒之后显示延时信息 else setTimeout(run); // 否则重新调度 }); // 输出示例 // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100第一次定时器是立即执行的接下来可以看到9, 15, 20, 24...——两次调用之间被强制插入了至少 4 毫秒的延时数组元素之间的差值。如果改用setInterval也会出现类似情况setInterval(f)会以零延时运行几次f随后以 4 毫秒以上的强制延时运行。这个限制源自远古时代许多现有脚本都依赖它所以机制保留至今。需要强调的是这是浏览器环境特有的限制。对于服务端的 JavaScript 并没有这一限制Node.js 还提供了setImmediate等其他调度即时异步任务的方式。配合仓库练习定时器的实战验证本章在仓库中配有若干课后任务位于 08-settimeout-setinterval 目录它们是检验上述知识点的最佳实践样本。练习一每秒输出一个数字任务 每秒输出一次 要求编写函数printNumbers(from, to)每秒输出一个数字从from到to并分别用setInterval和嵌套的setTimeout两种方式实现。使用setInterval的解法solution.mdfunction printNumbers(from, to) { let current from; let timerId setInterval(function() { alert(current); if (current to) { clearInterval(timerId); } current; }, 1000); } // 用例 printNumbers(5, 10);使用嵌套setTimeout的解法function printNumbers(from, to) { let current from; setTimeout(function go() { alert(current); if (current to) { setTimeout(go, 1000); } current; }, 1000); } // 用例 printNumbers(5, 10);请注意这两种解法在第一个输出之前都有一段初始延迟——函数在 1000ms 之后才被第一次调用。如果希望函数立即运行可以在单独一行先调用一次go()再用setInterval安排后续调用function printNumbers(from, to) { let current from; function go() { alert(current); if (current to) { clearInterval(timerId); } current; } go(); // 立即输出第一个数字 let timerId setInterval(go, 1000); } printNumbers(5, 10);这个练习直观地印证了两个核心知识点clearInterval/clearTimeout必须拿到定时器标识符才能取消调度而嵌套setTimeout与setInterval在首次调用前都有初始延时这一点上行为一致。练习二setTimeout 会显示什么任务 setTimeout 会显示什么 提出了一个关于执行时机的经典问题let i 0; setTimeout(() alert(i), 100); // ? // 假设这段代码的运行时间 100ms for(let j 0; j 100000000; j) { i; }答案见 solution.md任何setTimeout都只会在当前代码执行完毕之后才会执行所以alert显示的i取值为100000000且回调一定在for循环执行完成后才运行即使设定的 100ms 延时早已到期。let i 0; setTimeout(() alert(i), 100); // 100000000 // 假设这段代码的运行时间 100ms for(let j 0; j 100000000; j) { i; }这个练习从侧面验证了事件循环的单线程 任务队列模型延时到期只是把回调放入宏任务队列真正执行还要等当前同步代码跑完。理解了这一点就不会写出依赖定时器在循环中途插入执行的错误逻辑。总结setTimeout(func, delay, ...args)和setInterval(func, delay, ...args)允许我们在delay毫秒之后运行func一次或以delay毫秒为间隔周期性运行func。要取消函数的执行应调用clearInterval/clearTimeout并将setInterval/setTimeout返回的标识符作为入参传入。嵌套的setTimeout比setInterval更灵活既可以根据执行结果动态调整下一次调度的间隔也能更精确地控制两次执行之间的时间setInterval的实际间隔会被函数执行时间侵蚀。零延时调度setTimeout(func, 0)与setTimeout(func)相同用于调度需要尽快执行的调用但实际执行会在当前脚本完成后进行。浏览器会将setTimeout或setInterval五层及以上嵌套调用的最小延时限制在 4ms这是历史遗留的行为服务端 JavaScript 不受此限制。最后需要特别提醒所有的调度方法都不能保证确切的延时。即使延时参数写得很精确实际触发时间也可能受多种因素影响而变慢例如CPU 过载浏览器页签处于后台模式笔记本电脑处于省电模式。这些因素可能将定时器的最小分辨率最小延迟提升到 300ms 甚至 1000ms具体取决于浏览器及其设置。因此在设计依赖定时器精度的业务逻辑如倒计时、节流、轮询时务必把延时不精确作为前提必要时以Date.now()或performance.now()的实际时间戳校准而不是想当然地依赖定时器的绝对精度。赞分享文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载相关推荐深入理解 JavaScript 定时调度setTimeout 与 setInterval 实战指南深入理解 JavaScript 定时调度setTimeout 与 setInterval 实战指南 导读 在 JavaScript 开发中我们常常需要延迟文档/教程前端Modern JavaScript 教程实战setTimeout 与 setInterval 定时调度——实现每秒输出数字的 printNumbers 任务Modern JavaScript 教程实战setTimeout 与 setInterval 定时调度——实现每秒输出数字的 printNumbers 任务文档/教程前端JavaScript 数字全指南Number 类型、精度陷阱与 Math 工具现代 JavaScript 教程JavaScript 数字全指南Number 类型、精度陷阱与 Math 工具现代 JavaScript 教程 导读 本文基于现代 JavaScript文档/教程前端上一篇isomorphic-git 深入解析readTag 读取与解析 annotated tag 对象的完整指南下一篇CANN/GE ES可选输入样例创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询