JavaScript 单线程模型

理解 JavaScript 单线程、事件循环和任务队列

问题

解释 JavaScript 的单线程模型,以及它如何处理异步操作。

解答

什么是单线程

JavaScript 只有一个主线程执行代码,同一时间只能做一件事。

// 同步代码按顺序执行
console.log('1');
console.log('2');
console.log('3');
// 输出: 1, 2, 3

为什么是单线程

JavaScript 最初设计用于操作 DOM。如果多线程同时修改 DOM,会产生竞态条件:

// 假设多线程场景(实际不存在)
// 线程1: document.getElementById('app').remove()
// 线程2: document.getElementById('app').innerHTML = 'hello'
// 结果不可预测

事件循环(Event Loop)

单线程通过事件循环处理异步操作:

console.log('1'); // 同步,立即执行

setTimeout(() => {
  console.log('2'); // 宏任务,放入宏任务队列
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // 微任务,放入微任务队列
});

console.log('4'); // 同步,立即执行

// 输出顺序: 1, 4, 3, 2

执行顺序

// 完整示例
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
// setTimeout

宏任务与微任务

// 宏任务: setTimeout, setInterval, I/O, UI rendering
// 微任务: Promise.then, MutationObserver, queueMicrotask

setTimeout(() => console.log('宏任务1'), 0);
setTimeout(() => console.log('宏任务2'), 0);

Promise.resolve().then(() => console.log('微任务1'));
Promise.resolve().then(() => console.log('微任务2'));

// 输出:
// 微任务1
// 微任务2
// 宏任务1
// 宏任务2

事件循环流程图

┌───────────────────────────┐
│        调用栈 (Call Stack) │
└───────────────────────────┘
            ↓ 执行完毕
┌───────────────────────────┐
│      微任务队列 (全部执行)  │  ← Promise.then, queueMicrotask
└───────────────────────────┘
            ↓ 清空后
┌───────────────────────────┐
│      宏任务队列 (取一个)    │  ← setTimeout, setInterval
└───────────────────────────┘
            ↓ 执行完毕
          重复循环

async/await 的执行顺序

async function async1() {
  console.log('async1 start');
  await async2(); // await 后面的代码相当于放入微任务队列
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');
async1();
console.log('script end');

// 输出:
// script start
// async1 start
// async2
// script end
// async1 end

关键点

  • 单线程原因:避免多线程操作 DOM 产生竞态条件
  • 事件循环:调用栈 → 微任务队列(清空)→ 宏任务队列(取一个)→ 循环
  • 微任务优先:每次宏任务执行完,先清空所有微任务
  • 宏任务:setTimeout、setInterval、I/O
  • 微任务:Promise.then、queueMicrotask、MutationObserver