Skip to content

为什么需要事件循环?

JavaScript 是一门单线程的编程语言,这意味着它只有一个主线程来处理所有任务。如果没有事件循环机制,那么:

  1. 阻塞问题: 耗时操作会导致后续代码无法执行
  2. 资源浪费: CPU 在等待 I/O 时完全闲置
  3. 用户体验: 界面卡顿,无法响应用户交互

为了解决这些问题,JavaScript 引入了事件循环机制,将任务分为同步和异步两种执行模式。

二、事件循环的核心组成

1. 调用栈(Call Stack)

javascript
function foo() {
    console.log('foo');
    bar();
}

function bar() {
    console.log('bar');
}

foo();
// 调用栈变化过程:
// 1. [] -> [foo]
// 2. [foo] -> [foo, bar]
// 3. [foo, bar] -> [foo]
// 4. [foo] -> []

2. 堆(Heap)

  • 存储对象、数组等引用类型数据
  • 进行内存分配的区域
  • 由垃圾回收器自动管理

3. 任务队列(Task Queue)

分为两种类型:

宏任务(Macrotask)

javascript
// 常见宏任务
- script(整体代码)
- setTimeout/setInterval
- setImmediate(Node)
- requestAnimationFrame(浏览器)
- I/O操作
- UI rendering
- MessageChannel

微任务(Microtask)

javascript
// 常见微任务
- Promise.then/catch/finally
- process.nextTick(Node)
- MutationObserver
- queueMicrotask()
- IntersectionObserver

三、事件循环详细执行流程

  1. 执行同步代码

    • 将同步代码压入调用栈
    • 按顺序执行
    • 清空调用栈
  2. 执行微任务

    • 检查微任务队列
    • 将所有微任务依次压入调用栈执行
    • 清空微任务队列
  3. 执行宏任务

    • 检查宏任务队列
    • 取出一个宏任务执行
    • 执行完后重复步骤 2
  4. UI 渲染

    • 如果需要,执行 UI 渲染
    • 渲染完成后重复步骤 1

四、Promise 与事件循环

1. Promise 的执行时机

javascript
console.log('1');

new Promise((resolve) => {
    console.log('2');
    resolve();
}).then(() => {
    console.log('3');
});

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

console.log('5');

// 输出: 1 2 5 3 4

解析:

  • Promise 构造函数是同步执行的
  • .then() 回调是微任务
  • setTimeout 是宏任务

2. async/await 的执行机制

javascript
async function foo() {
    console.log('1');
    await Promise.resolve('2');
    console.log('3');  // 微任务
}

foo();
console.log('4');

// 输出: 1 4 3

await 的特点:

  • await 后面的代码相当于放到 Promise.then 中执行
  • 会暂停当前 async 函数的执行
  • 让出执行线程

五、复杂案例分析

案例 1: 多重嵌套

javascript
console.log('script start');

setTimeout(() => {
    console.log('timeout1');
    Promise.resolve().then(() => {
        console.log('promise1');
    });
}, 0);

new Promise((resolve) => {
    console.log('promise2');
    resolve();
    setTimeout(() => {
        console.log('timeout2');
    }, 0);
}).then(() => {
    console.log('promise3');
});

console.log('script end');

// 输出:
// script start
// promise2
// script end
// promise3
// timeout1
// promise1
// timeout2

案例 2: async/await 与 Promise 混合

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

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

console.log('script start');

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

async1();

new Promise((resolve) => {
    console.log('promise3');
    resolve();
}).then(() => {
    console.log('promise4');
});

console.log('script end');

// 输出:
// script start
// async1 start
// promise1
// promise3
// script end
// promise2
// promise4
// async1 end
// setTimeout

六、Node.js 事件循环的特点

Node.js 的事件循环与浏览器有所不同,它分为 6 个阶段:

  1. timers: 执行 setTimeout/setInterval 的回调
  2. pending callbacks: 执行系统操作的回调
  3. idle, prepare: 仅系统内部使用
  4. poll: 获取新的 I/O 事件
  5. check: 执行 setImmediate 的回调
  6. close callbacks: 执行 close 事件的回调
javascript
// Node.js 特有的执行顺序
setImmediate(() => {
    console.log('setImmediate');
});

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

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

// 可能的输出1:
// nextTick
// setTimeout
// setImmediate

// 可能的输出2:
// nextTick
// setImmediate
// setTimeout

七、实践建议

  1. 合理使用微任务
javascript
// 不推荐
setTimeout(() => {
    // 处理逻辑
}, 0);

// 推荐
queueMicrotask(() => {
    // 处理逻辑
});
  1. 注意性能问题
javascript
// 避免无限循环的微任务
function badCode() {
    Promise.resolve().then(() => badCode());
}
  1. 代码可读性
javascript
// 推荐使用 async/await 替代 Promise 链
// 不推荐
getData()
    .then(processData)
    .then(saveData)
    .catch(handleError);

// 推荐
async function handleData() {
    try {
        const data = await getData();
        const processed = await processData(data);
        await saveData(processed);
    } catch (err) {
        handleError(err);
    }
}

通过深入理解事件循环机制,我们可以:

  • 更好地控制代码执行顺序
  • 优化应用性能
  • 避免常见的异步编程陷阱
  • 写出更可维护的代码

这些知识在面试中经常被考察,建议多加练习和实践。