Skip to content

JS Event Loop / Promise / Worker

JavaScript 的并发核心不是“多线程共享内存”,而是 单线程事件循环 + microtask/macrotask 调度 + Worker 隔离计算

一句话定义

JS 默认在单线程里执行同步代码,通过 event loop 调度异步回调;Promise.then 属于 microtasksetTimeout/I/O 回调属于 macrotask;真要把 CPU 密集任务从主线程移走,要靠 Worker

为什么需要

如果只会说“JS 是单线程”,通常会在下面这些问题上答得含糊:

  • 为什么 Promise.thensetTimeout(..., 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 timeout

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());

它依然可能卡主线程。因为 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