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)更早执行? - 为什么页面会卡住,明明代码里已经用了异步?
- Web Worker 和 Flutter isolate、Go goroutine 有什么区别?
- 为什么前端里的“异步”大多不是并行执行?
这些问题和 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');典型输出:
text
1 sync
2 sync
3 promise then
4 timeout2. 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());它依然可能卡主线程。因为 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