1. 为什么 JavaScript 是单线程?

JavaScript 诞生于 1995 年 Netscape 浏览器,为了解决网页的交互需求

JavaScript 的设计初衷是操作网页(DOM),单线程可以避免多个线程同时操作页面导致的竞态和一致性问题。这也决定了 JavaScript 本质上只能同时做一件事

核心设计目标

  • 操作 DOM,保证渲染过程一致性
  • 容易实现,减少并发带来的“竞态、死锁、线程安全”等问题

单线程的好处:

  • 编码简单,开发者不用担心两个线程同时读写 DOM
  • 避免了同步、加锁等复杂机制
  • 性能损耗更小(线程切换/调度成本低)

单线程的局限:

  • 一次只能做一件事,阻塞任务会导致页面卡死
  • 不能直接利用多核 CPU 做并行计算

2. 异步的本质:事件循环(Event Loop)

JavaScript 通过事件循环机制实现异步。JS 引擎本身只有一个主线程(执行栈),但浏览器(或 Node.js)环境在底层配合有任务队列与事件触发:

  • 主线程:执行同步代码
  • 任务队列(Task Queue):存放待执行的异步回调(如 setTimeout、Promise.then、I/O 完成等)
  • 事件循环(Event Loop):主线程空闲时,不断检查任务队列,将回调取出并执行

直观图示(伪代码流程)

graph LR
A[主线程执行同步代码] --> B{遇到异步任务?}
B --是--> C[注册回调, 放入任务队列]
B --否--> D[继续执行主线程]
C --> D
D --> E{主线程空闲?}
E --是--> F[事件循环取队列回调, 推入主线程]
E --否--> D

执行顺序——核心原则:

  1. 同步代码先执行
  2. 处理所有微任务队列(Promise.then/catch/finally)
  3. 取出下一个宏任务,回到第1步

3. 事件循环执行流程与实验

flowchart TD
    MainThread[同步代码入栈/出栈] --> CheckMicro[微任务队列全部执行]
    CheckMicro --> NextMacro[取下一个宏任务]
    NextMacro --> MainThread

更详细的典型流程:

flowchart TD
    Sync[执行同步代码] --> MicroLoop{微任务队列有任务?}
    MicroLoop -- 有 --> Micro1[执行一个微任务]
    Micro1 --> MicroLoop
    MicroLoop -- 没有 --> Macro[取出宏任务]
    Macro --> Sync

2.1 宏任务与微任务的实际表现

宏任务(Macro Task)

  • setTimeout/setInterval
  • I/O 回调
  • UI 渲染/事件
  • script(整体脚本执行)

微任务(Micro Task)

  • Promise.then/catch/finally
  • MutationObserver
  • queueMicrotask

微任务总是优先于宏任务调度。

不妨做个实验:

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('promise1');
}).then(() => {
  console.log('promise2');
});

console.log('script end');

输出顺序:

script start
script end
promise1
promise2
undefined    控制台回显返回值同步结束
setTimeout

解释:

  • 同步代码先执行
  • 所有微任务(promise1、promise2)在 setTimeout 前全部执行完

与 Java 多线程的本质区别

JavaScriptJava 多线程
并发方式单线程 + 事件循环多线程 (多CPU核心并行)
任务调度由事件循环与任务队列驱动由 OS 线程调度(抢占/协作式)
内存隔离单线程无并发共享问题线程间需同步/锁
常见异步I/O、定时器、Promise线程、线程池、Future、Executor
并行能力真正并行需借助 Web Worker多线程可并行
编码模式Promise、async/await、回调synchronized、Future、Lock

结论

  • JavaScript 异步 = “伪并发”,单线程保证安全,事件循环负责调度。
  • Java 真正多线程 = “真并发”,线程间数据同步复杂。

延伸阅读