微任务和宏任务到底谁先跑?12 段 JS 代码跑出真实执行顺序

举报
deli007 发表于 2026/10/12 00:51:48 2026/10/12
【摘要】 微任务和宏任务到底谁先跑?12 段 JS 代码跑出真实执行顺序

图 1:事件循环可视化实验室首屏。左边是 12 段代码样例,中间是代码编辑器和同步、微任务、宏任务三个阶段面板,右边是预设对照表,每行都写着正确顺序、常见错误顺序和原因

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 先走,然后是微任务 A,A 里塞进去的 B 排在 C 前面。

看清楚这条的原因,比背十条结论有用。

一、12 段样例覆盖了什么

页面左边是可点击切换的样例列表,右边是预设对照表。

# 主题 真实输出顺序
1 同步代码 vs setTimeout 0 A → C → B
2 Promise.then vs setTimeout C → B → A
3 await 一个已 resolve 的 Promise B → A
4 queueMicrotask 的插入位置 A → C → B
5 await 与 Promise.then 的先后 C → A → B
6 for 循环里的 setTimeout Done → 0 → 1 → 2
7 多个 then 的排队顺序 C → A → B
8 async 函数里 await 前后 A → C → B
9 微任务里再产生微任务 D → A → B → C
10 Promise.all 与单个 then Done → A → A,B
11 setTimeout 0 与 1ms C → A → B
12 then 返回普通值与 Promise Done → A → B

这 12 行的顺序全部是页面跑出来的,不是我按记忆写的。

对照表里还有一列"常见错误",写的是最容易赌错的那个顺序。

跑完一段,页面把实际顺序和预期顺序摆在一起比,给出预测正确或错误,外加一个匹配率。

二、三个阶段怎么分出来的

中间那三个面板不是装饰,每一步输出都会落进其中一个。

顺序是这样的:代码同步执行时打出的记录进"同步阶段",当前这一轮排队的记录进"微任务队列",定时器回调里打出的记录进"宏任务队列"。

判断依据是运行时标记,不是靠猜。

同步阶段一开始就是真,等主函数返回了才切到微任务;只要进过 setTimeout 的回调,就切到宏任务。

2 号样例跑完的分工是这样:C 在同步阶段,B 在微任务队列,A 在宏任务队列。

三个面板各自只有一个色块,看一眼就知道这一行归谁。

图 2:2 号样例跑完后的界面。同步阶段面板是 C,微任务队列是 B,宏任务队列是 A,下方真实输出序列写着 0ms、1ms、1ms,右下角匹配率 100%

9 号样例更能说明问题:同步阶段只有 D,微任务队列排着 A、B、C 三块,宏任务队列是空的。

图 3:9 号样例跑完后的界面。微任务队列里是 A、B、C 三个色块,宏任务队列显示这次运行没有宏任务输出,输出序列是 0ms、1ms、1ms、2ms

三、验证与踩坑:12 条断言,和我在验收时改掉的五处

页面底部有一个"运行自检"按钮。

点一下,12 段代码真的跑一遍,每条都列出预期、实际和结论。

结果写在最下面:共 12 条断言:通过 12 条,失败 0 条,也就是 12/12 通过。

图 4:自检面板跑完的结果。12 条断言逐条给出预期顺序、实际顺序和绿色通过标记,底部一条写着共 12 条断言:通过 12 条,失败 0 条

第一版交付的页面跑通了,但验收时我改了五处。

第一处,队列面板活不过 100 毫秒。

第一版在运行结束后就把三个面板清空,我等一秒再看,面板已经是空的。

改成保留结果,下一次运行开始时才清。

第二处,微任务面板收下了所有输出。

第一版把最近三条记录一股脑塞进微任务面板,同步打出的 C 也在里面。

现在按真实阶段归属渲染,同步、微任务、宏任务各进各的面板。

第三处,报错时上一次的结果被抹掉。

第一版一进运行函数就清空输出区,代码写错的那一刻,上一段跑好的顺序也没了。

改成先做运行保护和语法校验,两道都过了才清空。

第四处,死循环保护的写法太窄,报错文案也不对。

第一版只匹配 while(true) 和 for(;;),而且这条提示被挂在"语法错误"上,死循环根本不是语法错误。

现在换成正则判断,文案改成"运行保护"。

第五处,数字被打上了引号。

6 号样例真实输出的是数字 0、1、2,输出行却渲染成 record(‘0’)。

现在字符串加引号,数字不加。

四、失败路径:语法错误和死循环

第一种失败是代码写坏。

我把编辑器改成 setTimeout(() => record(‘X’), 0) 后面多打一个反括号,再点运行。

页面顶部立刻出红字:语法错误:Unexpected token ‘)’。

关键是下面没有白屏,上一次跑出来的 C、B、A 和三个面板都还在。

图 5:语法错误时的界面。编辑器下方红字写着语法错误: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(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。

浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwsf&sourcead=dmzntgwwbwz,登录后直接对话,不用装软件。

我提交给它的需求是这样一段话:

做一个单文件网页应用 index.html:JavaScript 事件循环可视化实验室。页面内置 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
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。