程序员面试宝典

一站式面试准备平台

返回分类
JavaScript高级

JavaScript 事件循环与并发模型

深入理解 JavaScript 事件循环、任务队列、微任务队列及并发模型

2026-03-27
阅读时间: 13分钟

JavaScript 事件循环与并发模型

JavaScript 是单线程语言,但通过事件循环机制实现了非阻塞异步操作。理解事件循环是深入掌握 JavaScript 的关键。

JavaScript 执行模型

单线程模型

JavaScript 采用单线程执行模型,同一时刻只能执行一个任务。这避免了复杂的线程同步问题。

javascript
console.log('1');

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

console.log('3');

// 输出顺序:1 -> 3 -> 2

为什么 JavaScript 是单线程?

  • 浏览器环境: DOM 操作基于单一线程,多线程会导致复杂的同步问题
  • 历史原因: 最初设计时为了简化浏览器脚本

事件循环机制

执行栈与任务队列

┌─────────────────────────────────────┐
│           执行栈 (Call Stack)        │
│     执行同步代码和函数调用           │
└──────────────┬──────────────────────┘
               │
               ▼ 当执行栈为空时
┌─────────────────────────────────────┐
│         事件循环 (Event Loop)        │
│   不断检查执行栈和任务队列的状态      │
└──────────────┬──────────────────────┘
               │
       ┌───────┴───────┐
       ▼               ▼
┌──────────────┐ ┌──────────────────┐
│  宏任务队列   │ │   微任务队列      │
│ (MacroTask)  │ │   (MicroTask)   │
│ setTimeout   │ │   Promise.then  │
│ setInterval  │ │   queueMicrotask │
│ I/O 操作     │ │   MutationObserver│
└──────────────┘ └──────────────────┘

执行顺序

  1. 执行同步代码
  2. 执行栈清空后,优先处理所有微任务
  3. 微任务队列清空后,取出一个宏任务执行
  4. 重复以上步骤
javascript
console.log('1');

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

Promise.resolve().then(() => console.log('3'));

console.log('4');

// 分析:
// 1. console.log('1')     -> 输出 1
// 2. setTimeout 是宏任务   -> 加入宏任务队列
// 3. Promise.then 是微任务 -> 加入微任务队列
// 4. console.log('4')    -> 输出 4
// 5. 微任务队列:['3']    -> 输出 3
// 6. 宏任务队列:['2']    -> 输出 2
// 输出:1 -> 4 -> 3 -> 2

宏任务与微任务详解

宏任务(Macrotask)

API说明
setTimeout定时器
setInterval间隔执行
setImmediateNode.js 立即执行
I/O 操作文件读写、网络请求
UI 渲染浏览器渲染(浏览器环境)
requestAnimationFrame动画帧

微任务(Microtask)

API说明
Promise.then/catch/finallyPromise 回调
queueMicrotask()手动添加微任务
MutationObserverDOM 变化观察
process.nextTickNode.js 微任务

经典面试题分析

javascript
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

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

console.log('script start');

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

async1();

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

console.log('script end');

// 详细分析:
// 1. console.log('script start')           -> 输出 script start
// 2. setTimeout 加入宏任务队列
// 3. async1() 调用
//    - console.log('async1 start')         -> 输出 async1 start
//    - await async2() 立即执行 async2()
//      - console.log('async2')             -> 输出 async2
//      - await 暂停 async1,将后续代码作为微任务加入队列
// 4. new Promise() 是同步的
//    - console.log('promise1')             -> 输出 promise1
//    - resolve() 后,.then 加入微任务队列
// 5. console.log('script end')             -> 输出 script end
// 6. 微任务队列:['async1后续', 'promise2']
//    - async1后续                         -> 输出 async1 end
//    - promise2                            -> 输出 promise2
// 7. 宏任务队列:['setTimeout']
//    - setTimeout                          -> 输出 setTimeout
//
// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

Node.js 中的事件循环

Node.js 事件循环阶段

   ┌───────────────────────────┐
   │        timers             │  执行 setTimeout 和 setInterval 回调
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │   pending callbacks       │  执行延迟到下一个循环迭代的 I/O 回调
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │     idle, prepare         │  内部使用
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │         poll              │  检索新的 I/O 事件,执行 I/O 回调
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │        check              │  执行 setImmediate 回调
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │    close callbacks         │  执行关闭回调(如 socket.on('close'))
   └───────────────────────────┘

setTimeout vs setImmediate

javascript
// Node.js 环境中
setTimeout(() => {
    console.log('timeout');
}, 0);

setImmediate(() => {
    console.log('immediate');
});

// 输出不确定,取决于系统性能
// 但如果放在 I/O 回调内,setImmediate 总是先执行

process.nextTick vs setImmediate

javascript
Promise.resolve()
    .then(() => console.log('promise.then'));

process.nextTick(() => console.log('process.nextTick'));

// 输出:
// process.nextTick  (nextTick 比微任务更高优先级)
// promise.then

常见面试问题

Q1: 为什么 setTimeout(fn, 0) 不一定会立即执行?

即使延迟为 0,setTimeout 也会被加入宏任务队列。只有当执行栈和微任务队列都为空时,才会执行。

javascript
setTimeout(() => console.log('timeout'), 0);

Promise.resolve().then(() => {
    // 长时间运行的同步代码
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
        sum += i;
    }
    console.log('promise resolved');
});

console.log('sync code');

// 输出:sync code -> promise resolved -> timeout

Q2: async/await 和 Promise 微任务的关系?

async/await 是 Promise 的语法糖:

javascript
async function example() {
    await promise;
    console.log('after await');
}

// 相当于
function example() {
    return promise.then(() => {
        console.log('after await');
    });
}

await 之后的代码会被包装成微任务。

Q3: 如何理解 JavaScript 的"非阻塞"?

javascript
// 长时间计算不会阻塞主线程... 错!
// JavaScript 是单线程,长时间同步计算会阻塞 UI

// ❌ 错误:阻塞主线程
function heavyCalc() {
    let result = 0;
    for (let i = 0; i < 10000000000; i++) {
        result += i;
    }
    return result;
}

// ✅ 正确:使用 Web Worker(浏览器)或 Worker Threads(Node.js)

Q4: requestAnimationFrame vs setTimeout 动画?

特性requestAnimationFramesetTimeout/setInterval
帧率与屏幕刷新率同步(60fps)需手动控制
页面隐藏自动暂停继续运行
节流内置需手动实现
性能优化合并可能丢帧
javascript
// 使用 requestAnimationFrame 实现平滑动画
function animate(element) {
    let position = 0;
    
    function step() {
        position += 5;
        element.style.transform = `translateX(${position}px)`;
        
        if (position < 500) {
            requestAnimationFrame(step);
        }
    }
    
    requestAnimationFrame(step);
}

最佳实践

  1. 避免长时间同步计算,使用 Web Worker
  2. 合理使用微任务,不要用微任务处理太复杂的逻辑
  3. 动画使用 requestAnimationFrame,不要用 setTimeout
  4. setTimeout(fn, 0) 不是立即执行,只是延迟到下一个宏任务
  5. Node.js 中注意 poll 阶段的 I/O 回调,可能阻塞事件循环

相关标签