Skip to content

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. 先执行同步代码,输出 1 sync
  2. 注册一个 setTimeout macrotask
  3. 注册一个 Promise.then microtask
  4. 继续执行同步代码,输出 2 sync
  5. 当前同步栈清空后,先清空 microtask queue,输出 3 promise then
  6. 下一轮事件循环再取出 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 / WebDart / FlutterAndroid / KotlinBackend
默认执行模型单线程 event loop单 isolate event loop多线程 + 协程/Looper线程池/协程/事件循环按框架而定
更高优先级队列microtask (Promise.then)microtask (Future.microtask)无统一对应无统一对应
普通异步队列macrotask (setTimeout)event queue (Future(() {}))Handler/Executor/DispatcherExecutor/事件源
真正并行Workerisolate / Isolate.runThread / Default DispatcherThread / process

常见场景 ​

1. 页面初始化顺序错乱 ​

例如:

  • 你以为 setTimeout(..., 0) 会最先跑
  • 实际上 Promise.then 更早

这和 Dart Future.microtask vs Future(() {}) 的问题几乎是同类。

2. 大 JSON 解析导致页面卡顿 ​

即使你写成:

js
Promise.resolve().then(() => heavyParse());

可能执行顺序

  1. 同步阶段把 then 回调加入 microtask queue
  2. 当前同步代码结束后,事件循环立即取出这个 microtask
  3. heavyParse() 仍在主线程直接执行,直到计算结束才会让出执行权
  4. 这段时间里,后续输入事件、渲染机会、普通异步回调都可能被延后

可能输出 / 外在表现

  • 这段代码本身不一定打印日志
  • 更常见的现象是页面短暂卡顿、点击无响应、滚动掉帧,或 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.thenmicrotask快速衔接异步结果
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/

复习检查题 ​

  1. 为什么 Promise.then 往往早于 setTimeout(..., 0)?

    答:因为 Promise.then 回调进入 microtask queue,而 setTimeout 进入 macrotask queue;每轮事件循环会先清空 microtask,再处理下一个 macrotask。

  2. 为什么 Promise 不能直接解决 CPU 密集任务导致的卡顿?

    答:因为 Promise 默认仍在主线程事件循环体系里调度,只是改变执行时机,不会自动创建独立线程或隔离上下文。

  3. Web Worker 和 Dart isolate 的共同点是什么?

    答:都强调隔离执行单元与消息传递,不共享主执行上下文里的普通对象,适合把 CPU 密集任务移出主执行线程。

速记 ​

  • JS 默认单线程 event loop
  • Promise.then = microtask
  • setTimeout = macrotask
  • Promise 不是线程
  • CPU 重任务要上 Worker

站点构建时间:2026/8/24 23:43:17