一、核心背景:为什么需要Event Loop?
JavaScript 是单线程语言,意味着主线程一次只能执行一个任务。而渲染进程的主线程需要处理大量工作(如JS执行、页面渲染、DOM事件、网络请求等),若所有任务都排队等待同步执行,会导致耗时任务(如网络请求、定时器)阻塞后续代码,造成页面卡顿、交互无响应。
为解决单线程的阻塞问题,JS 引入了「消息队列 + Event Loop」的执行机制,将任务分类处理,实现异步执行,确保主线程高效运转。
二、核心概念:任务分类
根据执行优先级和特性,任务被分为「同步代码」「微任务」「宏任务」三类,三者执行顺序有严格规则。
1. 同步代码
「主线程优先执行」的任务,逐行执行、阻塞后续代码,执行完毕后才会处理异步任务。
核心特点:立即执行,无延迟,执行顺序与代码书写顺序一致。
示例(来自文档):
console.log('同步代码 1');
const promise1 = new Promise((resolve) => {
console.log('Promise 构造函数'); // Promise构造函数内代码是同步的
resolve();
console.log('Promise 构造函数内 resolve 后'); // 同步执行
});
async function asyncFn() {
console.log('async 函数同步部分'); // async函数内,await之前的代码是同步的
}
asyncFn();
console.log('同步代码 2');
2. 微任务(Microtask)
「同步代码执行完后,宏任务执行前」优先执行的异步任务,优先级高于宏任务,且会一次性清空整个微任务队列(包括执行微任务过程中新增的微任务)。
常见类型(结合文档):
- Promise.then/catch/finally(Promise构造函数内代码是同步的,then回调是微任务)
- await 后面的代码(本质是Promise.then的回调,属于微任务)
- queueMicrotask() 手动创建的微任务
- MutationObserver(前端特有,监听DOM变化的微任务)
示例(来自文档):
// Promise.then 微任务
promise1.then(() => {
console.log('Promise.then 1');
});
// await 后微任务
await Promise.resolve();
console.log('await 后微任务');
// queueMicrotask 微任务
queueMicrotask(() => {
console.log('queueMicrotask 微任务');
});
// MutationObserver 微任务
const observer = new MutationObserver(() => {
console.log('MutationObserver 微任务');
});
3. 宏任务(Macrotask)
耗时较长、优先级较低的异步任务,同步代码和微任务执行完毕后才会执行,每次只执行一个宏任务,执行完后需再次检查微任务队列。
常见类型(结合文档):
- 定时器:setTimeout、setInterval(文档重点示例)
- script 标签(整个JS脚本本身是一个宏任务)
- DOM事件回调(如click、resize,前端特有)
- 网络请求回调(AJAX)
补充说明:文档中提到的「页面渲染(重绘重排)、动画、垃圾回收」不属于队列任务,但会与宏任务、微任务抢占主线程,若JS执行时间过长,会导致页面卡顿、动画掉帧。
三、核心规则:Event Loop 执行顺序(重中之重)
Event Loop 的执行流程是固定的,按以下步骤循环执行,直到所有任务完成:
- 先执行主线程中的同步代码,直到主线程为空(调用栈清空);
- 清空微任务队列中的所有任务(包括执行微任务时新增的微任务);
- 从宏任务队列中取出一个任务执行(每次只执行一个);
- 执行完这个宏任务后,再次检查并清空所有微任务;
- 重复步骤 3-4,循环往复,直到宏任务队列和微任务队列均为空。
四、实战解析:结合文档代码理解执行顺序
以下是文档中完整的JS代码,结合执行规则分析输出顺序,快速掌握Event Loop实战逻辑:
console.log('同步代码 1');
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => {
console.log('setTimeout 1 内部微任务');
});
}, 0);
const promise1 = new Promise((resolve) => {
console.log('Promise 构造函数');
resolve();
console.log('Promise 构造函数内 resolve 后');
});
promise1.then(() => {
console.log('Promise.then 1');
setTimeout(() => {
console.log('Promise.then 1 内部 setTimeout');
}, 0);
});
async function asyncFn() {
console.log('async 函数同步部分');
await Promise.resolve();
console.log('await 后微任务');
}
asyncFn();
console.log('同步代码 2');
queueMicrotask(() => {
console.log('queueMicrotask 微任务');
});
const observer = new MutationObserver(() => {
console.log('MutationObserver 微任务');
});
const div = document.createElement('div');
observer.observe(div, { attributes: true });
div.setAttribute('data-test', '1'); // 触发 MutationObserver
执行步骤拆解:
- 执行同步代码,输出: 同步代码 1 → Promise 构造函数 → Promise 构造函数内 resolve 后 → async 函数同步部分 → 同步代码 2
- 同步代码执行完毕,清空微任务队列,输出: Promise.then 1 → await 后微任务 → queueMicrotask 微任务 → MutationObserver 微任务
- 微任务清空,执行第一个宏任务(setTimeout 1),输出:setTimeout 1
- 执行完该宏任务,检查并清空其内部产生的微任务,输出:setTimeout 1 内部微任务
- 微任务再次清空,执行下一个宏任务(Promise.then 1 内部的 setTimeout),输出:Promise.then 1 内部 setTimeout
- 所有任务执行完毕,循环结束。
最终输出顺序(重点记忆):
同步代码 1
Promise 构造函数
Promise 构造函数内 resolve 后
async 函数同步部分
同步代码 2
Promise.then 1
await 后微任务
queueMicrotask 微任务
MutationObserver 微任务
setTimeout 1
setTimeout 1 内部微任务
Promise.then 1 内部 setTimeout
五、重点总结(必背)
- 执行优先级:同步代码 > 微任务 > 宏任务;
- script 标签本身是一个宏任务,JS 代码的执行从这个宏任务开始;
- 微任务队列会一次性清空,宏任务队列每次只执行一个;
- Promise 构造函数内的代码是同步的,只有 then/catch/finally 回调是微任务;
- await 后面的代码会被转为微任务,等待 await 右侧的 Promise resolved 后执行;
- 页面渲染、动画等非队列任务会与异步任务抢占主线程,避免JS执行时间过长。
六、学习提示
Event Loop 是 JS 异步编程的核心,掌握其执行顺序是解决异步回调、定时器、Promise、async/await 相关问题的关键。建议多结合实战代码(如文档中的示例)拆解执行步骤,反复练习,就能快速掌握。