QuickJS 顶层 await 为何只执行两次:事件循环挂起原因与修复
【免费下载链接】quickjsPublic repository of the QuickJS Javascript Engine.项目地址: https://gitcode.com/gh_mirrors/qu/quickjs
在 QuickJS 里,同一段等待逻辑,写成顶层 await 只输出两次;写成.then()则每秒稳定打印一次。差别不在写法对错,而在事件循环对 Promise 的处理方式。
🔁 复现场景:触发 QuickJS 顶层 await 卡顿的三个条件
触发这个行为并不神秘:
const p = createPipe(); p.readHandler = onRead; // 读处理器一直未关闭 setTimeout(() => p.write("x"), 5000); // 5 秒后写入数据 // 主循环不断创建短期 Promise(1 秒超时后关闭管道) await waitForClose(p); // 顶层 await三样东西要同时在场:管道的读处理器没有关掉;一个 5 秒的setTimeout悬着;主循环不断新建短期 Promise,每个都会在 1 秒后关管道。少掉任何一样,输出节奏通常不会坏。
.then() vs 顶层 await:两种写法的行为对照
事件序列完全一样,差别只在等待的写法:
| 写法 | 实际表现 |
|---|---|
.then()链式 | 每秒打印一次时间戳,节奏正常 |
顶层await | 只出现两行输出,都在 5 秒内落地 |
写法 A 相当于事件循环一直空转着等事;写法 B 相当于循环中途被按了暂停,直到 5 秒定时器到期才重新醒来。
🧩 QuickJS 顶层 await 卡住的原因:唤醒逻辑的三处变化
分歧来自 commit 8405876,它改了引擎判断"能不能睡"的方式。
顶层 await 的续体不再算待办信号
先给结论:8405876 之后,模块在顶层 await 处挂起时,引擎不再把它的续体记成一次待办活动。.then()链里每挂一个回调都是给循环的提示,循环会每个 tick 照常运转;换成 await 后,这个提示消失了,引擎判断无事可做,直接停 tick,只剩下 5 秒的定时器在撑着。
微任务冲刷的时机错位
Promise 结算、微任务队列冲刷时,顶层 await 的续体被排到了和.then()不同的执行点。引擎复查"还有没有活跃处理器"的时机因此被推迟,本该 1 秒内处理的短期 Promise 全部挤在这个空档里排队。
读处理器成了沉睡的守夜人
管道读处理器本该是维持循环的最后实体。旧逻辑里,数据一到它就叫醒循环;新逻辑里它的触发频率被压低,两次写入之间的 5 秒窗口内,循环几乎不发声。
🛠 修复路径与版本跟进
后续有单独的提交把这个行为修了回去,改动集中在三处:顶层 await 挂起/恢复的路径被重排,引擎在挂起后会重新检查待办状态;唤醒条件把顶层 await 的续体也算了进去;Promise 结算时的上下文恢复一并修正。升级时留意 Changelog 里 Promise 与事件循环相关的条目;落在 8405876 之后、修复之前的版本上,跑定时器加 await 的用例要格外小心。
📋 QuickJS 异步卡住排查:三条实战建议
- 对时间敏感的链路用
.then()链或显式回调,顶层 await 只做入口。 - 在 Promise 超时回调里顺手关闭管道读处理器,干完活就注销,别留给循环空等。
- 引擎升级后,先回归一批含定时器加 await 的异步用例,对比升级前后的输出节奏。
顶层 await 的卡顿,是引擎处理 Promise 与事件循环的一扇小窗。分清"处理器、定时器、微任务"这三者谁在维持循环醒来,这类问题基本就能定位。
【免费下载链接】quickjsPublic repository of the QuickJS Javascript Engine.项目地址: https://gitcode.com/gh_mirrors/qu/quickjs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考