事件循环机制的核心是协调 JavaScript 单线程运行时,处理异步任务和事件回调的执行顺序,使其不阻塞主线程。
关键要点:
-
单线程 + 异步:JavaScript 是单线程的,但通过事件循环机制,能够“非阻塞”地处理 I/O 等异步操作。
-
两个队列:
- 宏任务队列 (MacroTask Queue / Task Queue) :包含整体 script 代码、
setTimeout、setInterval、I/O 操作、UI 渲染、setImmediate(Node.js)、requestAnimationFrame(浏览器) 等。 - 微任务队列 (MicroTask Queue / Job Queue) :包含
Promise.then/catch/finally、process.nextTick(Node.js)、MutationObserver(浏览器)、queueMicrotask等。微任务的优先级高于宏任务。
- 宏任务队列 (MacroTask Queue / Task Queue) :包含整体 script 代码、
-
执行流程 (浏览器环境简化版) :
-
同步代码执行:首先执行全局 script 代码(这是一个宏任务)。执行过程中,遇到同步代码立即执行,遇到异步 API 则根据类型处理:
- 宏任务:将其回调推入宏任务队列。
- 微任务:将其回调推入微任务队列。
-
清空微任务:当前宏任务执行完毕后,会立即、依次、彻底地清空当前微任务队列中的所有任务(包括在执行这些微任务过程中新产生的微任务,也会继续执行,直到队列为空)。
-
渲染 (如有需要) :如果浏览器需要更新视图,会执行 UI 渲染。
-
取下一个宏任务:从宏任务队列中取出一个任务执行,然后重复“清空微任务 -> (可能渲染) -> 取下一个宏任务”的循环。
-
一个经典的面试题例子:
console.log('1'); // 同步代码
setTimeout(() => {
console.log('2'); // 宏任务回调
Promise.resolve().then(() => {
console.log('3'); // 微任务回调
});
}, 0);
Promise.resolve().then(() => {
console.log('4'); // 微任务回调
setTimeout(() => {
console.log('5'); // 宏任务回调
}, 0);
});
console.log('6'); // 同步代码
输出顺序:1, 6, 4, 2, 3, 5
拆解步骤:
- 执行全局 script(宏任务1):打印
1和6。 - 遇到
setTimeout,回调函数推入宏任务队列。 - 遇到
Promise.resolve().then,回调函数推入微任务队列。 - 当前宏任务执行完毕,立即清空微任务队列:执行微任务,打印
4,同时遇到内部的setTimeout,其回调推入宏任务队列。 - 取下一个宏任务(来自第2步的
setTimeout回调,宏任务2):打印2,遇到内部的Promise.then,其回调推入微任务队列。 - 关键:每个宏任务执行完毕后,都要立即清空微任务队列。所以,此时立即执行刚加入的微任务,打印
3。 - 取下一个宏任务(来自第4步的
setTimeout回调,宏任务3):打印5。
问: 介绍一下 JavaScript 事件循环
答:
- JavaScript 是单线程的,事件循环使其能处理异步。
- 任务分为宏任务和微任务,微任务优先级更高。
- 主线程(同步代码)执行 -> 清空所有微任务 -> 取一个宏任务执行 -> 再清空所有微任务 -> ... 如此循环。
- 在浏览器中,每次事件循环可能伴随着 UI 渲染。