微任务和宏任务到底谁先跑?12 段 JS 代码跑出真实执行顺序 Promise.then 和 setTimeout 谁先跑背过八股的人都能答上来微任务先宏任务等下一轮。我把这句话拆成了 12 段代码让浏览器自己跑一遍。每跑完一段页面把真实执行顺序按毫秒打出来再和服务端写在样例里的预期对一次。12 段全部跑完12 条断言全部对上一条没差。成品只有一个 index.html双击就能跑不联网、不引第三方库。案例目录在这里下载下来直接打开https://atomgit.com/deli007/demo_park/tree/main/codearts-event-loop-visualizer为什么值得看顺序你背得下来毫秒数背不下来先看最短的一段。setTimeout(() record(A), 0); Promise.resolve().then(() record(B)); record(C);页面上跑出来的顺序是 C、B、A时间戳 0ms、1ms、1ms。A 写在第一行排在最后。它比 B 只晚了 1 毫秒但性质完全不同B 在当前这一轮就处理掉了A 要排到下一轮。这三个数字是浏览器现场给的不是我抄的。换成 9 号样例微任务里再排微任务顺序是 D、A、B、C。同步代码 D 先走然后是微任务 AA 里塞进去的 B 排在 C 前面。看清楚这条的原因比背十条结论有用。一、12 段样例覆盖了什么页面左边是可点击切换的样例列表右边是预设对照表。#主题真实输出顺序1同步代码 vs setTimeout 0A → C → B2Promise.then vs setTimeoutC → B → A3await 一个已 resolve 的 PromiseB → A4queueMicrotask 的插入位置A → C → B5await 与 Promise.then 的先后C → A → B6for 循环里的 setTimeoutDone → 0 → 1 → 27多个 then 的排队顺序C → A → B8async 函数里 await 前后A → C → B9微任务里再产生微任务D → A → B → C10Promise.all 与单个 thenDone → A → A,B11setTimeout 0 与 1msC → A → B12then 返回普通值与 PromiseDone → A → B这 12 行的顺序全部是页面跑出来的不是我按记忆写的。对照表里还有一列常见错误写的是最容易赌错的那个顺序。跑完一段页面把实际顺序和预期顺序摆在一起比给出预测正确或错误外加一个匹配率。二、三个阶段怎么分出来的中间那三个面板不是装饰每一步输出都会落进其中一个。顺序是这样的代码同步执行时打出的记录进同步阶段当前这一轮排队的记录进微任务队列定时器回调里打出的记录进宏任务队列。判断依据是运行时标记不是靠猜。同步阶段一开始就是真等主函数返回了才切到微任务只要进过 setTimeout 的回调就切到宏任务。2 号样例跑完的分工是这样C 在同步阶段B 在微任务队列A 在宏任务队列。三个面板各自只有一个色块看一眼就知道这一行归谁。9 号样例更能说明问题同步阶段只有 D微任务队列排着 A、B、C 三块宏任务队列是空的。三、验证与踩坑12 条断言和我在验收时改掉的五处页面底部有一个运行自检按钮。点一下12 段代码真的跑一遍每条都列出预期、实际和结论。结果写在最下面共 12 条断言通过 12 条失败 0 条也就是 12/12 通过。第一版交付的页面跑通了但验收时我改了五处。第一处队列面板活不过 100 毫秒。第一版在运行结束后就把三个面板清空我等一秒再看面板已经是空的。改成保留结果下一次运行开始时才清。第二处微任务面板收下了所有输出。第一版把最近三条记录一股脑塞进微任务面板同步打出的 C 也在里面。现在按真实阶段归属渲染同步、微任务、宏任务各进各的面板。第三处报错时上一次的结果被抹掉。第一版一进运行函数就清空输出区代码写错的那一刻上一段跑好的顺序也没了。改成先做运行保护和语法校验两道都过了才清空。第四处死循环保护的写法太窄报错文案也不对。第一版只匹配 while(true) 和 for(;而且这条提示被挂在语法错误上死循环根本不是语法错误。现在换成正则判断文案改成运行保护。第五处数字被打上了引号。6 号样例真实输出的是数字 0、1、2输出行却渲染成 record(‘0’)。现在字符串加引号数字不加。四、失败路径语法错误和死循环第一种失败是代码写坏。我把编辑器改成 setTimeout(() record(‘X’), 0) 后面多打一个反括号再点运行。页面顶部立刻出红字语法错误Unexpected token ‘)’。关键是下面没有白屏上一次跑出来的 C、B、A 和三个面板都还在。第二种失败是死循环。我把代码换成 let n 0; while (1) { n; }点运行。页面给出红字运行保护检测到可能的死循环写法这段代码没有执行。这条保护是正则匹配只挡 while (1)、while (true)、for (; 这几种最容易写出来的写法。换成 while (n 1e9) 这种有条件的写法仍然会真的跑满主线程页面会卡住。改之前那一版是字符串匹配while (1) 能直接绕过去我实测点下去标签页就不再响应了。自检那 12 条断言也有边界要说清楚。期望值是我按规范写死的断言只能证明浏览器实际输出等于我写的预期证明不了预期本身一定对。12 条里第 1 条和第 4 条的顺序完全一样第 5 条和第 7 条也一样覆盖度没有看上去那么满。阶段归属也是运行时标记推出来的定时器回调里再产生的微任务会被记成宏任务。12 段样例里没有这种写法所以不影响结论但换成别的代码就会不准。五、本地复现页面是单文件不需要构建。python3 -m http.server 8080浏览器打开 http://localhost:8080/index.html或者直接双击 index.html。想核对上面的数字先点运行自检看到 12 条全过再回到 2 号样例单跑一遍。单跑的时候盯三个地方输出序列的毫秒、三个面板各是谁、右下角的匹配率。六、准备环境进入码道 Web码道有三种使用方式WebUI浏览器对话、TUI终端命令行和桌面 IDEIDE 插件。本文用 WebUI 版演示。浏览器打开码道 Web 版https://devcloud.cn-north-4.huaweicloud.com/chat?sourcedmzntgwsfsourceaddmzntgwwbwz登录后直接对话不用装软件。我提交给它的需求是这样一段话做一个单文件网页应用 index.htmlJavaScript 事件循环可视化实验室。页面内置 12 段 JS 代码样例覆盖同步代码与 setTimeout 0 的顺序、Promise.then 与 setTimeout 的顺序、await 一个已 resolve 的 Promise 之后代码的时机、queueMicrotask 的插入位置、await 与 Promise.then 的先后、for 循环里的 setTimeout、多个 then 的排队顺序、async 函数里 await 前后的输出、微任务里再产生微任务的排队、Promise.all 与单个 then 的顺序、setTimeout 嵌套 0 与 1 毫秒的顺序、then 返回普通值和 Promise 的差别。代码里用 record(A) 这样的调用记录每一步。中间要有运行按钮点击后用 new Function 真实执行代码真实调用 setTimeout、Promise、queueMicrotask把每一步实际执行的先后顺序按序号记录到输出区。左边是可点击切换的样例列表中间上方是调用栈、微任务队列、宏任务队列三个面板中间下方是带毫秒的真实输出序列。右边是预设对照表给出正确顺序和常见错误猜测顺序运行完自动比对并给出匹配率。底部要有运行自检按钮把 12 段代码全部真跑一遍逐条列出断言最后汇总显示共 12 条断言、通过多少条、失败多少条。允许编辑代码语法错误或死循环要给中文红色提示并保留上一次的有效结果不能白屏。界面中文深色主题不要外部依赖和网络请求。完成后打开预览让我直接看到运行效果。七、使用码道体会这次最值钱的动作是把验收口径写进需求里。我在需求里点名要一个可点的自检按钮要求它把预期、实际、通过与否都渲染在页面上。交付时就有了 12 条可点可验的断言文章里的数字才有出处。第二点是要求真跑而不是显示正确答案。代码用 new Function 现场执行顺序由浏览器给页面只是记录者。这一点决定了这篇文章里的毫秒数是不是真的。第三点是截图要自己逐张放大看。队列面板活不过 100 毫秒、报错时结果被抹掉这两个问题读代码很容易滑过去截图一看就露。第四点是边界要如实写。自检里那两条重复顺序、阶段归属的局限、死循环保护只挡几种写法写出来比藏起来有用。八、总结微任务先于宏任务这句话只值一行。真正能带走的是那三个数字同步代码 0ms微任务在同一轮里 1ms 就处理完宏任务要等下一轮。想验证自己有没有记错就跑一遍自检再挑 2 号和 9 号单跑看三个面板各是谁。案例目录下载下来双击就能跑https://atomgit.com/deli007/demo_park/tree/main/codearts-event-loop-visualizer