Appearance
JS Event Loop / Promise / Worker
JavaScript 的并发核心不是“多线程共享内存”,而是 单线程事件循环 + microtask/macrotask 调度 + Worker 隔离计算。
一句话定义
JS 默认在单线程里执行同步代码,通过 event loop 调度异步回调;Promise.then 属于 microtask,setTimeout/I/O 回调属于 macrotask;真要把 CPU 密集任务从主线程移走,要靠 Worker。
为什么需要
如果只会说“JS 是单线程”,通常会在下面这些问题上答得含糊:
- 为什么
Promise.then比setTimeout(..., 0)更早执行?- 一句话答:
then回调进 microtask queue,setTimeout进 macrotask queue;每轮事件循环先清空 microtask,再取下一个 macrotask。
- 一句话答:
- 为什么页面会卡住,明明代码里已经用了异步?
- 一句话答:异步只改调度时机;若重活仍在主线程执行(如在 Promise 里做解析 / 计算),主线程被占住,渲染与事件响应照样卡。
- Web Worker 和 Flutter isolate、Go goroutine 有什么区别?
- 一句话答:Worker / isolate 是独立执行单元(各自线程 + 独立堆,靠消息传递);goroutine 是同进程内由 Go runtime 调度的轻量任务,共享内存。
- 为什么前端里的“异步”大多不是并行执行?
- 一句话答:同一线程事件循环的异步只是“延后 / 分段”,同一时刻仍在主线程串行执行;只有 Web Worker 这类独立单元才真正并行。
这些问题和 Flutter / Dart 的 event loop、Android 主线程不卡顿、后端事件驱动框架的理解是连着的。
底层机制
1. 先同步,后 microtask,再 macrotask
一个最常见的顺序模型:
text
同步代码
→ 清空 microtask queue
→ 取一个 macrotask 执行
→ 再清空 microtask queue
→ 下一轮 macrotask最小示例:
js
console.log('1 sync');
setTimeout(() => console.log('4 timeout'), 0);
Promise.resolve().then(() => console.log('3 promise then'));
console.log('2 sync');可能执行顺序
- 先执行同步代码,输出
1 sync - 注册一个
setTimeoutmacrotask - 注册一个
Promise.thenmicrotask - 继续执行同步代码,输出
2 sync - 当前同步栈清空后,先清空 microtask queue,输出
3 promise then - 下一轮事件循环再取出
setTimeout,输出4 timeout
可能输出
text
1 sync
2 sync
3 promise then
4 timeout预期现象
- 即使
setTimeout(..., 0)写得更早,Promise.then仍通常更早执行 - 这个例子验证的是事件循环优先级,不是“哪个 API 注册得早就先跑”
观察重点
- 先区分同步代码、microtask、macrotask 三层
Promise.then改变的是回调进入哪条队列,不是创建了新线程
2. Promise 不是线程,只是更高优先级异步回调
很多人把 Promise 想成“后台线程返回结果”,其实不对。默认情况下:
- Promise 仍在主线程事件循环体系内
then/catch/finally回调进入 microtask queue- 它解决的是异步结果编排,不解决 CPU 密集任务抢主线程
3. Worker 才是把重计算移出主线程
Web Worker 特点:
- 独立线程/上下文
- 不共享 DOM
- 通过消息传递通信
- 适合大计算、解析、压缩等 CPU 密集任务
和 Dart isolate 很像的一点是:隔离内存 + 消息传递;但 API 形态和宿主能力不同。
Android / Flutter / Web / Backend 对照
| 维度 | JS / Web | Dart / Flutter | Android / Kotlin | Backend |
|---|---|---|---|---|
| 默认执行模型 | 单线程 event loop | 单 isolate event loop | 多线程 + 协程/Looper | 线程池/协程/事件循环按框架而定 |
| 更高优先级队列 | microtask (Promise.then) | microtask (Future.microtask) | 无统一对应 | 无统一对应 |
| 普通异步队列 | macrotask (setTimeout) | event queue (Future(() {})) | Handler/Executor/Dispatcher | Executor/事件源 |
| 真正并行 | Worker | isolate / Isolate.run | Thread / Default Dispatcher | Thread / process |
常见场景
1. 页面初始化顺序错乱
例如:
- 你以为
setTimeout(..., 0)会最先跑 - 实际上
Promise.then更早
这和 Dart Future.microtask vs Future(() {}) 的问题几乎是同类。
2. 大 JSON 解析导致页面卡顿
即使你写成:
js
Promise.resolve().then(() => heavyParse());可能执行顺序
- 同步阶段把
then回调加入 microtask queue - 当前同步代码结束后,事件循环立即取出这个 microtask
heavyParse()仍在主线程直接执行,直到计算结束才会让出执行权- 这段时间里,后续输入事件、渲染机会、普通异步回调都可能被延后
可能输出 / 外在表现
- 这段代码本身不一定打印日志
- 更常见的现象是页面短暂卡顿、点击无响应、滚动掉帧,或 DevTools 里看到主线程长任务
预期现象
- 把重计算包进 Promise,不会自动获得并行执行能力
- 如果
heavyParse()足够重,UI 依然会被阻塞
观察重点
- Promise 解决的是结果编排,不是 CPU 隔离
- 只要重计算还留在主线程,写成 microtask 反而可能更快抢到主线程时间片
它依然可能卡主线程。因为 Promise 只是调度方式,不是新线程。真正的方向是:
- Worker
- 分块计算
- 把 CPU 任务移出主线程
3. H5 / WebView 与 App 宿主协作
混合开发里,JS 的主线程卡顿会直接表现为:
- H5 页面掉帧
- WebView 交互迟滞
- 桥接响应变慢
这和 Android 主线程、Flutter UI isolate 卡顿是同一个底层问题:主执行线程被 CPU 任务占住。
常见坑
| 坑 | 现象 | 修法 |
|---|---|---|
| 把 Promise 当线程 | 误判性能瓶颈 | 分清“异步回调”与“真正并行” |
| 滥用 microtask | 回调链太密,界面响应变差 | 只在必须抢优先级时使用 |
用 setTimeout(..., 0) 当精确调度 | 顺序不稳定、被事件循环影响 | 用明确的任务边界与状态管理 |
| CPU 重任务全放主线程 | 卡顿、掉帧、点击失效 | 用 Worker / 分块处理 |
与相近概念对比
| 概念 | 本质 | 适合场景 |
|---|---|---|
Promise.then | microtask | 快速衔接异步结果 |
setTimeout(..., 0) | macrotask | 延后到下一轮事件循环 |
| Worker | 隔离线程/上下文 | CPU 密集任务 |
| Dart isolate | 隔离内存并发单元 | Flutter/Dart 重计算 |
| Kotlin 协程 | 可挂起任务,不等于新线程 | 多线程调度下的异步编排 |
对应实验
目前本主题先以理论文档为主,后续建议补:
labs/node/runtime-concurrency/js-event-loop-order/labs/node/runtime-concurrency/js-worker-basic/
复习检查题
为什么
Promise.then往往早于setTimeout(..., 0)?答:因为
Promise.then回调进入 microtask queue,而setTimeout进入 macrotask queue;每轮事件循环会先清空 microtask,再处理下一个 macrotask。为什么 Promise 不能直接解决 CPU 密集任务导致的卡顿?
答:因为 Promise 默认仍在主线程事件循环体系里调度,只是改变执行时机,不会自动创建独立线程或隔离上下文。
Web Worker 和 Dart isolate 的共同点是什么?
答:都强调隔离执行单元与消息传递,不共享主执行上下文里的普通对象,适合把 CPU 密集任务移出主执行线程。
速记
- JS 默认单线程 event loop
Promise.then= microtasksetTimeout= macrotask- Promise 不是线程
- CPU 重任务要上 Worker