我们首先需要理解JavaScript的事件循环(Event Loop)机制,因为它是JavaScript异步编程的核心。事件循环允许JavaScript在执行非阻塞I/O操作时保持高效,尽管它是单线程的。
事件循环的基本概念:
JavaScript运行时包含一个消息队列(或任务队列),用于存储待处理的消息(任务)。每个消息都关联着一个回调函数。事件循环会不断地从消息队列中取出消息并执行对应的回调函数。
事件循环中有两种主要的任务队列:
宏任务(Macrotasks):包括整体script代码、setTimeout、setInterval、I/O、UI渲染等。
微任务(Microtasks):包括Promise回调、MutationObserver、process.nextTick(Node.js)等。
事件循环的执行顺序:
执行一个宏任务(从宏任务队列中取出一个任务执行)。
执行过程中遇到微任务,将其添加到微任务队列。
宏任务执行完毕,立即执行当前微任务队列中的所有微任务(依次执行)。
微任务执行完毕,开始下一个宏任务(如果有的话)。
如此循环,直到所有任务完成。
常见问题:
1. 请解释事件循环(Event Loop)是什么?
事件循环是JavaScript处理异步操作的一种机制。它通过一个循环不断地检查任务队列中是否有任务需要执行,如果有则取出执行。它分为宏任务和微任务,每次执行一个宏任务后,会清空整个微任务队列。
2. 宏任务(Macrotask)和微任务(Microtask)有什么区别?
宏任务:由宿主环境(浏览器、Node.js)发起的任务,如setTimeout、setInterval、I/O、UI渲染、事件回调等。
微任务:由JavaScript引擎发起的任务,如Promise回调、MutationObserver、process.nextTick(Node.js)等。
执行顺序:每次事件循环中,先执行一个宏任务,然后执行所有微任务,再执行下一个宏任务,如此循环。
3. 以下代码的输出顺序是什么?
console.log('1');
setTimeout(function() {
console.log('2');
}, 0);
Promise.resolve().then(function() {
console.log('3');
});
console.log('4');
输出顺序:1, 4, 3, 2
解释:
首先执行同步代码:输出1和4。
然后检查微任务队列,有Promise回调,输出3。
最后执行宏任务队列中的setTimeout回调,输出2。
4. 如果嵌套宏任务和微任务,执行顺序如何?
console.log('start');
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise3');
});
console.log('end');
输出顺序:start, end, promise3, timeout1, promise1, timeout2, promise2
解释:
同步代码:输出start和end。
微任务队列:执行Promise回调,输出promise3。
宏任务队列:第一个setTimeout回调,输出timeout1,然后其内部的Promise回调加入微任务队列,执行微任务(输出promise1)。
接着执行第二个setTimeout回调,输出timeout2,然后其内部的Promise回调加入微任务队列,执行微任务(输出promise2)。
5. setTimeout(fn, 0)
真的在0毫秒后执行吗?
不一定。它表示在至少0毫秒后执行,即尽快执行,但实际执行时间取决于当前执行栈是否为空以及消息队列中是否有其他任务在等待。因为JavaScript是单线程的,如果当前有任务在执行,那么setTimeout的回调必须等待。
6. Node.js中的事件循环和浏览器中的事件循环有什么区别?
在Node.js中,事件循环分为多个阶段(如timers、pending callbacks、idle, prepare、poll、check、close callbacks),每个阶段执行特定的任务。而浏览器中的事件循环没有这样的阶段划分,但同样有宏任务和微任务的概念。
在Node.js中,process.nextTick
回调比微任务还要优先执行,即在每个阶段切换时都会执行process.nextTick
队列。
7. 什么是任务队列(Task Queue)和作业队列(Job Queue)?
任务队列通常指宏任务队列,而作业队列通常指微任务队列。在ECMAScript规范中,微任务队列被称为Job Queue。
8. 如何理解异步编程中的“饥饿”问题?
如果微任务中不断地产生新的微任务,那么宏任务将永远得不到执行,导致“饥饿”。因此,在编写代码时要注意避免微任务的无限循环。
总结:
事件循环是JavaScript异步的基础,理解宏任务和微任务的执行顺序是解决异步代码执行顺序问题的关键。通常会通过代码输出题来考察对事件循环的理解。同时,也要注意Node.js和浏览器环境在事件循环上的差异
JavaScript 事件循环(Event Loop)深度解析与高频问题
一、事件循环核心概念
JavaScript 是单线程语言,事件循环是其实现异步编程的核心机制:
┌───────────────────────┐
┌─>│ 调用栈 │<───执行上下文
│ └──────────┬────────────┘
│ │
│ ┌──────────┴────────────┐
│ │ 微任务队列 │<───Promise/MutationObserver/process.nextTick
│ └──────────┬────────────┘
│ │
│ ┌──────────┴────────────┐
│ │ 宏任务队列 │<───setTimeout/setInterval/I/O/UI渲染/事件回调
│ └──────────┬────────────┘
│ │
└─────────────┘
二、执行顺序规则
- 同步代码:优先执行调用栈中的同步任务
- 微任务:当调用栈清空后,执行所有微任务
- 宏任务:每次从宏任务队列取一个任务执行
- 渲染:在宏任务之间执行 UI 渲染
三、高频题及解析
1. 基础执行顺序题
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => console.log('4'));
console.log('5');
输出顺序:1 → 5 → 4 → 2 → 3
解析:
- 同步代码:1, 5
- 微任务:4
- 宏任务(setTimeout):2
- 宏任务中的微任务:3
2. 混合微任务与宏任务
setTimeout(() => console.log('A'), 0);
Promise.resolve().then(() => {
console.log('B');
setTimeout(() => console.log('C'), 0);
});
Promise.resolve().then(() => console.log('D'));
console.log('E');
输出顺序:E → B → D → A → C
解析:
- 同步代码:E
- 微任务队列:
- 第一个 Promise:B(添加宏任务C)
- 第二个 Promise:D
- 宏任务队列:
- A(先进入队列)
- C(后进入队列)
3. async/await 执行顺序
async function async1() {
console.log('A');
await async2();
console.log('B');
}
async function async2() {
console.log('C');
}
console.log('D');
setTimeout(() => console.log('E'), 0);
async1();
new Promise(resolve => {
console.log('F');
resolve();
}).then(() => console.log('G'));
console.log('H');
输出顺序:D → A → C → F → H → B → G → E
解析:
-
await
后面的代码相当于放在Promise.then
中 - 等价转换:
// async1 转换为: function async1() { console.log('A'); new Promise(resolve => { async2(); resolve(); }).then(() => console.log('B')); }
4. Node.js 与浏览器差异
setTimeout(() => console.log('1'), 0);
setImmediate(() => console.log('2'));
process.nextTick(() => console.log('3'));
Promise.resolve().then(() => console.log('4'));
console.log('5');
浏览器输出:5 → 4 → 1 → 2 → 3(nextTick 非标准)
Node.js 输出:5 → 3 → 4 → 1 或 5 → 3 → 4 → 2 → 1
差异点:
-
process.nextTick
优先级最高(Node独有) -
setImmediate
与setTimeout(0)
顺序不确定 - Node 有多个阶段:timers → pending → idle → poll → check → close
5. 复杂嵌套场景
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise 1'));
}, 0);
setTimeout(() => {
console.log('Timeout 2');
Promise.resolve().then(() => {
console.log('Promise 2');
setTimeout(() => console.log('Timeout 3'), 0);
});
}, 0);
Promise.resolve().then(() => console.log('Promise 3'));
console.log('End');
输出顺序:
Start → End → Promise 3 → Timeout 1 → Promise 1 → Timeout 2 → Promise 2 → Timeout 3
四、事件循环核心知识点
-
任务队列类型:
- 宏任务:script、setTimeout、setInterval、I/O、UI渲染
- 微任务:Promise.then、MutationObserver、process.nextTick(Node)
-
关键执行规则:
- 每执行一个宏任务后,清空整个微任务队列
- UI 渲染在宏任务之间执行
-
requestAnimationFrame
在渲染前执行
-
Node.js 特殊机制:
┌───────────────────────┐ │ timers │<── setTimeout/setInterval ├───────────────────────┤ │ pending callbacks │<── I/O回调 ├───────────────────────┤ │ idle, prepare │<── 内部使用 ├───────────────────────┤ │ poll │<── 检索新I/O事件 ├───────────────────────┤ │ check │<── setImmediate ├───────────────────────┤ │ close callbacks │<── 关闭事件回调 └───────────────────────┘
五、问题必备技巧
-
分析代码时先标记:
- [S] 同步代码
- [M] 宏任务
- [m] 微任务
-
解题步骤:
- 执行所有同步代码
- 执行所有微任务
- 执行一个宏任务
- 重复步骤2-3
-
常见陷阱:
// 阻塞事件循环 while (true) {} // 会阻塞所有任务 // 微任务递归 function recursiveMicrotask() { Promise.resolve().then(recursiveMicrotask); }
掌握事件循环机制是JavaScript高级开发的必备技能,建议通过Chrome DevTools的Performance面板实时观察调用栈执行过程加深理解。