Event Loop(事件循环)学习笔记

2 阅读5分钟

一、核心背景:为什么需要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 的执行流程是固定的,按以下步骤循环执行,直到所有任务完成:

  1. 先执行主线程中的同步代码,直到主线程为空(调用栈清空);
  2. 清空微任务队列中的所有任务(包括执行微任务时新增的微任务);
  3. 宏任务队列中取出一个任务执行(每次只执行一个);
  4. 执行完这个宏任务后,再次检查并清空所有微任务;
  5. 重复步骤 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. 执行同步代码,输出: 同步代码 1 → Promise 构造函数 → Promise 构造函数内 resolve 后 → async 函数同步部分 → 同步代码 2
  2. 同步代码执行完毕,清空微任务队列,输出: Promise.then 1 → await 后微任务 → queueMicrotask 微任务 → MutationObserver 微任务
  3. 微任务清空,执行第一个宏任务(setTimeout 1),输出:setTimeout 1
  4. 执行完该宏任务,检查并清空其内部产生的微任务,输出:setTimeout 1 内部微任务
  5. 微任务再次清空,执行下一个宏任务(Promise.then 1 内部的 setTimeout),输出:Promise.then 1 内部 setTimeout
  6. 所有任务执行完毕,循环结束。

最终输出顺序(重点记忆):

同步代码 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 相关问题的关键。建议多结合实战代码(如文档中的示例)拆解执行步骤,反复练习,就能快速掌握。