Skip to content

巧解事件循环:JavaScript 执行机制全解析

事件循环(Event Loop)是 JavaScript 最核心的概念之一,也是面试中的高频考点。

JavaScript 是单线程的,同一时间只能做一件事。那它是如何实现异步操作的?这就要靠事件循环。

一、为什么需要事件循环?

JavaScript 是单线程语言,意味着:

  • 同一时间只能执行一段代码
  • 如果一段代码执行时间过长,会阻塞后续代码执行

示例

javascript
console.log('开始');

setTimeout(() => {
    console.log('定时器执行');
}, 1000);

console.log('结束');
// 输出:开始 → 结束 → 定时器执行

如果没有事件循环,定时器会阻塞程序等待 1 秒。但 JavaScript 是怎么做到不等定时器完成就继续执行后续代码的?

答案:事件循环 + 任务队列

二、事件循环的核心机制

事件循环由三部分组成:

┌─────────────────────────┐
│        执行栈            │  ← 执行同步代码
│     (Call Stack)        │
└─────────────────────────┘


┌─────────────────────────┐
│       任务队列          │  ← 存放待执行的回调
│    (Task Queue)        │
│   ├─ 宏任务队列        │
│   └─ 微任务队列        │
└─────────────────────────┘


┌─────────────────────────┐
│       事件循环          │  ← 不断检查队列
│     (Event Loop)       │
│   执行栈空 → 取任务执行 │
└─────────────────────────┘

执行流程

  1. 执行栈执行同步代码
  2. 遇到异步任务,交给浏览器/Web API 处理
  3. 异步完成后,回调放入任务队列
  4. 事件循环不断检查执行栈
  5. 执行栈为空时,取队列中的任务执行
  6. 重复步骤 4-5

三、宏任务与微任务

任务队列分为两类:

类型示例执行时机
宏任务setTimeout、setInterval、I/O、UI渲染当前宏任务执行完后
微任务Promise.then、MutationObserver、queueMicrotask下一轮事件循环前

执行顺序

执行宏任务 → 执行完所有微任务 → 执行下一个宏任务 → ...

示例

javascript
console.log('1');

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

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

console.log('4');

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

执行过程

  1. 打印 1
  2. setTimeout 交给 WebAPI,callback 放入宏任务队列
  3. Promise.then 放入微任务队列
  4. 打印 4
  5. 执行栈为空,先执行所有微任务(打印 3
  6. 执行宏任务(打印 2

四、经典面试题

题目 1

javascript
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

题目 2(async/await):

javascript
async function async1() {
    console.log('async1 start');
    await async2();
    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

解析await 后面的代码相当于放在 Promise.then 中,属于微任务。

题目 3(综合):

javascript
console.log('1');

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

new Promise((resolve) => {
    console.log('4');
    resolve();
}).then(() => {
    console.log('5');
});

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

console.log('7');

答案

1
4
7
5
2
3
6

五、常见面试题解析

Q1:setTimeout(fn, 0) 是立即执行吗?

A:不是。fn 会加入宏任务队列,等待当前执行栈清空后执行,最少延迟 4ms 左右。

Q2:Promise 和 setTimeout 谁先执行?

A:Promise.then 是微任务,setTimeout 是宏任务。微任务优先级更高,会先执行。

Q3:async/await 原理是什么?

Aawait 暂停当前函数执行,等待 Promise 完成。await 后面的代码相当于 Promise.then 的回调,属于微任务。

javascript
async function foo() {
    console.log('a');
    await console.log('b');
    console.log('c');
}

// 等价于:
function foo() {
    console.log('a');
    Promise.resolve(console.log('b')).then(() => {
        console.log('c');
    });
}

六、实战建议

场景推荐做法
异步操作完成后再执行使用 Promise 或 async/await
延迟执行使用 setTimeout
需要等待多个异步使用 Promise.all
需要排队执行使用 async 串行
插队执行使用 queueMicrotask

七、总结

事件循环的核心要点:

  1. 单线程:JavaScript 同一时间只能做一件事
  2. 异步:通过 WebAPI + 回调实现
  3. 任务队列:宏任务和微任务
  4. 执行顺序:同步 → 微任务 → 宏任务
  5. 事件循环:不断检查执行栈和队列,循环往复

理解事件循环,才能写出正确的异步代码,也是面试必备技能!