资讯动态

JavaScript setTimeout 执行时机全解:为什么 100ms 定时回调总在同步代码之后运行

发布时间:2026/10/5 1:43:37 来源:尧图企业网站定制
文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载本篇文章围绕 Modern JavaScript Tutorialen.javascript.info中《Scheduling: setTimeout and setInterval》章节的一道高难度思考题展开完整剖析setTimeout回调的真实执行时机为什么即使设置了 100ms 延迟回调也总是要等当前脚本全部执行完毕之后才会触发。读完本文你将掌握定时器与事件循环Event Loop的协作机制、setTimeout/setInterval的完整 API 用法、嵌套定时器的间隔精度差异以及浏览器 4ms 最小延迟限制等实战要点。题目回顾这个 setTimeout 究竟会显示什么原题出自 4-settimeout-result/task.md题目代码如下let i 0; setTimeout(() alert(i), 100); // ? // assume that the time to execute this function is 100ms for(let j 0; j 100000000; j) { i; }题目给出三个候选答案在循环之后执行在循环之前执行在循环开始阶段执行。并追问alert最终会显示什么正确答案循环之后执行alert 显示 100000000根据 官方解答任何一个setTimeout都只会等当前代码全部执行完之后才运行。此时的i是最后一次递增后的值100000000。let i 0; setTimeout(() alert(i), 100); // 100000000 // assume that the time to execute this function is 100ms for(let j 0; j 100000000; j) { i; }两个关键结论回调在循环之后运行——即使循环耗时远超 100ms回调也不会插队alert显示的是循环结束后的最终值100000000——回调读取i的时刻发生在循环完成之后因此它看到的不是中途某个中间值。答案背后的核心原理定时器回调不会抢占当前脚本要理解上面的结论需要回到 JavaScript 的执行模型。浏览器以及 Node.js中的 JS 执行流程基于事件循环Event Loop参见 2-ui/99-ui-misc/03-event-loop/article.md。单线程与任务队列JavaScript 引擎是单线程的同一时刻只能执行一段代码。事件循环的通用算法极其简单只要队列中有任务就从最老的任务开始依次执行队列为空时睡眠等待直到新任务出现再回到第 1 步。以下都属于任务macrotask外部脚本script src...加载完成后执行它是任务用户移动鼠标派发mousemove事件并执行处理器是任务setTimeout计时到点时运行其回调也是任务。当引擎正忙于执行一段同步脚本时期间到期的事件、定时器并不会立即执行而是被放入宏任务队列macrotask queueV8 术语按先来先服务FIFO的顺序排队。当前脚本执行完毕引擎才会从队列头部取出下一个任务。同步代码永远先行回到题目setTimeout(() alert(i), 100)只是把回调注册进调度器并设定 100ms 的计时并没有真正执行它。紧接着的for循环是同步代码一旦开始执行引擎便全力运行它期间不会让出执行权。即便循环耗时超过 100ms定时器早已到期但它的回调只能作为新任务被丢进队列末端等待只有当庞大的for循环约 1 亿次递增跑完、当前脚本结束引擎才会取出队列中的定时器回调执行。因此回调必然在循环之后运行它读取i时循环已经完成i已经是100000000。这正是零延迟小节所强调的语义定时器回调只会在当前脚本彻底结束后才被调度执行。即使写成setTimeout(func)delay 为 0回调也是当前脚本之后尽快执行而不是立刻执行。setTimeout 基础回顾完整 API 用法与参数本节的完整讲解见 08-settimeout-setinterval/article.md。setTimeout允许我们安排函数在指定时间之后运行一次其完整语法为let timerId setTimeout(func|code, [delay], [arg1], [arg2], ...)各参数含义参数说明func|code要执行的函数或一段代码字符串。通常传函数字符串形式仅因历史原因保留不推荐使用delay运行前的延迟单位毫秒1000ms 1 秒默认 0arg1, arg2, ...传给函数的参数带参数的基本用法function sayHi(phrase, who) { alert( phrase , who ); } setTimeout(sayHi, 1000, Hello, John); // Hello, John如果第一个参数是字符串JS 会自动为其创建函数例如setTimeout(alert(Hello), 1000)也能工作但官方明确建议改用箭头函数setTimeout(() alert(Hello), 1000)。经典陷阱传函数引用而不是调用它新手常犯的错误是在函数后面加括号// 错误写法 setTimeout(sayHi(), 1000);setTimeout期待的是一个函数引用而sayHi()会立即执行该函数并把执行结果本例中为undefined传给setTimeout——结果什么都不会被调度。正确写法是不加括号setTimeout(sayHi, 1000)。用 clearTimeout 取消执行setTimeout返回一个定时器标识符timerId可用它取消执行let timerId setTimeout(...); clearTimeout(timerId);let timerId setTimeout(() alert(never happens), 1000); alert(timerId); // 定时器标识符 clearTimeout(timerId); alert(timerId); // 仍是同一个标识符取消后不会变成 null浏览器中timerId是一个数字在其他环境如 Node.js它可能是带有额外方法的对象。由于这些方法不属于 JavaScript 规范各环境实现略有差异是正常的。延迟 100ms不等于100ms 后一定执行题目中的delay 100只表示最早可执行时间而非保证的执行时刻。定时器的实际触发受事件循环状态制约当前同步任务越长回调被推迟得越久。事件循环章节的 使用场景一拆分 CPU 密集型任务 正好演示了这一点——一个从 1 数到 10 亿的同步循环会让浏览器卡死let i 0; let start Date.now(); function count() { // 做一个繁重的任务 for (let j 0; j 1e9; j) { i; } alert(Done in (Date.now() - start) ms); } count();该章节给出的解法是用零延迟的嵌套setTimeout把大任务拆成小块让引擎在每块之间喘口气得以响应点击事件、渲染界面。这也印证了同一个事实定时器回调永远无法打断正在执行的同步代码。深入实践setInterval 与嵌套 setTimeout 的对比setInterval按固定间隔重复执行setInterval语法与setTimeout相同但会按给定间隔定期重复运行函数直到调用clearInterval(timerId)停止let timerId setInterval(func|code, [delay], [arg1], [arg2], ...)下面的例子每 2 秒弹出一条消息5 秒后停止// 每 2 秒重复 let timerId setInterval(() alert(tick), 2000); // 5 秒后停止 setTimeout(() { clearInterval(timerId); alert(stop); }, 5000);需要注意在大多数浏览器包括 Chrome 和 Firefox中弹窗alert/confirm/prompt显示期间内部计时器仍在滴答走动。如果你迟迟不关掉弹窗下一个alert会在关闭的瞬间立即弹出实际间隔会小于 2 秒。嵌套 setTimeout更灵活、间隔更精确定时重复的另一种方式是嵌套setTimeout——在当前回调的末尾再调度下一次let timerId setTimeout(function tick() { alert(tick); timerId setTimeout(tick, 2000); // (*) 在本次回调末尾安排下一次 }, 2000);嵌套setTimeout比setInterval更灵活下一次调用可以根据当前这次的结果动态调整。例如一个每 5 秒向服务器请求数据的服务当服务器过载时可以把间隔翻倍为 10、20、40 秒……let delay 5000; let timerId setTimeout(function request() { ...send request... if (request failed due to server overload) { // 增大下一次运行的间隔 delay * 2; } timerId setTimeout(request, delay); }, delay);更重要的是嵌套setTimeout能更精确地控制两次执行之间的间隔。对比下面两段代码// setInterval 版本 let i 1; setInterval(function() { func(i); }, 100);// 嵌套 setTimeout 版本 let i 1; setTimeout(function run() { func(i); setTimeout(run, 100); }, 100);对setInterval来说内部调度器每 100ms 运行一次func(i)但func自身的执行时间会吃掉一部分间隔——真实的间隔其实小于 100ms。极端情况下如果func每次执行都超过delay调用之间将完全没有停顿而嵌套setTimeout由于新调用总是在上一次结束之后才安排可以保证固定的间隔此处为 100ms两张时序图取自同一章节目录 08-settimeout-setinterval直观展示了两种调度方式下调用时刻的差异。零延迟 setTimeout 与浏览器 4ms 限制零延迟当前脚本结束后尽快执行setTimeout(func, 0)或直接setTimeout(func)是一个特殊用法把回调安排得尽可能早但调度器只会在当前脚本执行完毕后才调用它。典型示例setTimeout(() alert(World)); alert(Hello);输出顺序是Hello然后World——第一行把调用放进 0ms 后的日程表但调度器要等当前脚本结束后才会查看日程表所以同步的Hello先出现。浏览器对嵌套定时器的 4ms 下限浏览器中存在历史遗留限制HTML Living Standard 规定嵌套定时器超过 5 层之后间隔被强制至少为 4 毫秒。下面这段自调度代码记录了每次调用的真实延迟let start Date.now(); let times []; setTimeout(function run() { times.push(Date.now() - start); // 记录距离上次调用的真实延迟 if (start 100 Date.now()) alert(times); // 100ms 后展示这些延迟 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...——4ms 的强制间隔开始生效。setInterval(f)也有类似行为前几次以零延迟运行之后按 4ms 间隔运行。此限制仅存在于浏览器端服务端 JS如 Node.js没有该限制并提供了setImmediate等替代调度方式。内存与垃圾回收注意事项把函数传给setInterval/setTimeout时调度器会创建对它的内部引用即使没有其他引用该函数也不会被垃圾回收直到它被调度执行对setTimeout或调用clearInterval对setInterval。// 该函数在调度器调用它之前一直驻留内存 setTimeout(function() {...}, 100);副作用在于函数引用其外层词法环境因此只要函数活着外层变量也会活着可能占据比函数本身大得多的内存。当不再需要某个定时器时即使它很小也最好显式取消它。巩固练习每秒输出一个数字两种实现同章节还有一道配套练习 1-output-numbers-100ms/task.md编写函数printNumbers(from, to)从from开始每秒输出一个数字直到to结束要求分别用setInterval和嵌套setTimeout实现。官方解答见 1-output-numbers-100ms/solution.md// 方案一setInterval function 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单独调用一次再启动定时器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);小结setTimeout(func, delay, ...args)与setInterval(func, delay, ...args)允许在delay毫秒后运行一次/定期运行func取消执行需要调用clearTimeout/clearInterval并传入setTimeout/setInterval返回的标识符所有定时器回调都只会在当前同步代码执行完毕之后运行——这是setTimeout显示 100000000这道题的核心结论嵌套setTimeout是比setInterval更灵活的替代方案能更精确地控制两次执行之间的间隔零延迟setTimeout(func, 0)用于尽快但在当前脚本之后执行常用于拆分 CPU 密集型任务、展示进度或延迟到事件完全处理后执行浏览器对 5 层以上的嵌套定时器及setInterval存在最小 4ms 的历史延迟限制所有调度方法都不保证精确延迟CPU 过载、标签页后台化、笔记本省电模式都可能把最小定时器分辨率提升到 300ms 甚至 1000ms。若要深入定时器与事件循环的更多实战场景拆分重型计算、进度条、宏任务与微任务的区别可继续阅读 事件循环章节。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐Sunshine 串流主机把书房的游戏 PC 变成客厅游戏舱10 分钟出画面Sunshine 串流主机把书房的游戏 PC 变成客厅游戏舱10 分钟出画面 周五晚上你想窝在沙发打电脑里的 3A 大作可游戏跑在书房的机器上人却走不文档/教程前端Metro运行时系统如何在设备上执行打包后的代码Metro运行时系统如何在设备上执行打包后的代码 Metro是React Native的JavaScript打包器负责将开发者的源代码转换为可在移动设构建工具移动开发CLIivy中的混合框架调试同时在多个后端运行代码进行对比ivy中的混合框架调试同时在多个后端运行代码进行对比 你是否还在为深度学习模型在不同框架间的行为差异而头疼训练时PyTorch结果完美部署到TensorF人工智能机器学习开发工具上一篇如何快速上手weibo-analysis-and-visualization3分钟搭建你的微博文本分析系统下一篇Vueuse/Motion 组合式 API 使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑