轻轻松松搞懂JavaScript事件循环机制

457 阅读4分钟

一、什么是事件循环

事件循环(Event Loop)是JavaScript实现非阻塞异步编程的核心机制。由于JavaScript是单线程语言,事件循环负责协调同步任务和异步任务的执行顺序,确保主线程不被阻塞。

单线程模型的特点

  • 同一时刻只能执行一个任务
  • 同步任务优先执行,完成后才处理异步任务
  • 耗时操作(如网络请求、定时器)会被放入任务队列等待执行

二、任务队列:宏任务与微任务

1. 微任务(Microtasks)

紧急任务,在同步任务执行完毕后立即执行,优先级高于宏任务。

微任务包括:MutationObserverPromise.then()catch()Promise为基础开发的其它技术,比如fetch APIV8的垃圾回收、Node独有的process.nextTickqueueMicrotask()等。

2. 宏任务(Macrotasks)

常规异步任务,在所有微任务执行完毕后才执行。

宏任务包括:scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

同步代码和宏任务的关系

同步代码是宏任务的组成部分 ,具体表现为:

  1. script 标签本身就是一个宏任务

    • 整个JavaScript文件的执行以宏任务形式启动
    • 宏任务内部首先执行所有同步代码
    • 同步代码执行过程中可能注册新的宏任务/微任务
  2. 宏任务的执行包含同步代码阶段

宏任务执行流程:
┌─────────────────────┐
│ 执行宏任务中的同步代码 │
├─────────────────────┤
│ 执行所有微任务       │
└─────────────────────┘
  • 每个宏任务执行时,会先运行其包含的同步代码
  • 同步代码执行完毕后,才会处理微任务队列

三、事件循环的执行流程

Snipaste_2025-07-11_15-14-53.png

  • 执行同步代码(全局script)
  • 执行所有微任务(按注册顺序)
  • 执行一个宏任务
  • 重复步骤2-3,形成循环

注意: 如果有多个宏任务要执行,按照先注册先执行原则,微任务也一样。

四、代码示例分析

示例1:基本执行顺序

<script>
  console.log("script start"); // 同步任务
  
  // 异步宏任务
  setTimeout(() => {
    console.log("setTimeout");
  }, 0);
  
  // 微任务
  Promise.resolve().then(() => {
    console.log('promise');
  });
  
  console.log("script end"); // 同步任务
</script>

我们按照顺序依次分析,先看第一句console.log("script start"); 它是同步任务,直接打印出来;

接着是setTimeout,它是异步宏任务,在下一轮执行;

Promise.then为微任务,先标记,不管;

console.log("script end"); 为同步任务,直接打印。

接着执行微任务,把Promise.then 的打印。

然后整体渲染一遍,进入下一轮,下一轮执行setTimeout的宏任务,打印setTimeout,再询问是否有微任务,发现没有,再渲染整个页面。

最终打印顺序依次为: script start script end promise setTimeout

示例2:多个任务队列

console.log('同步Start');

const promise1 = Promise.resolve('First Promise');
const promise2 = Promise.resolve('Second Promise');
const promise3 = new Promise(resolve => {
  console.log('promise3'); // 同步执行
  resolve('Third Promise');
});

promise1.then(value => console.log(value));
promise2.then(value => console.log(value));
promise3.then(value => console.log(value));

// 两个异步宏任务
setTimeout(() => {
  console.log('下一把再相见');
  Promise.resolve('Forth Promise').then(value => console.log(value));
}, 0);

setTimeout(() => {
  console.log('下下一把再相见');
}, 0);

console.log('同步end');

输出结果为:

同步Start
promise3
同步end
First Promise
Second Promise
Third Promise
下一把再相见
Forth Promise
下下一把再相见

注意:Promise本身是同步的,所有promise3会先打印出来。而Promise.then是异步的,所以Promise.then会作为微任务,在微任务阶段处理。

接着我们对执行流程进行分解。

  1. 宏任务阶段
// 执行顺序:从上到下依次执行同步代码
1. console.log('同步Start'); → 输出 "同步Start"
2. 定义promise1(Promise.resolve创建的已决议Promise,.then回调进入微任务队列)
3. 定义promise2(同上,.then回调进入微任务队列)
4. 定义promise3:
   - 执行Promise构造函数内的同步代码 → console.log('promise3') → 输出 "promise3"
   - 调用resolve('Third Promise') → 将promise3状态改为已决议
5. 为三个Promise注册.then回调(按顺序加入微任务队列)
6. 定义第一个setTimeout → 回调函数加入宏任务队列
7. 定义第二个setTimeout → 回调函数加入宏任务队列
8. console.log('同步end'); → 输出 "同步end"

第一轮宏任务阶段输出

同步Start
promise3
同步end

2. 微任务阶段

1. promise1.then → console.log('First Promise') → 输出 "First Promise"
2. promise2.then → console.log('Second Promise') → 输出 "Second Promise"
3. promise3.then → console.log('Third Promise') → 输出 "Third Promise"

第一轮微任务输出

First Promise
Second Promise
Third Promise

3. 第二轮宏任务执行阶段

宏任务队列中的第一个setTimeout
1. console.log('下一把再相见') → 输出 "下一把再相见"
2. Promise.resolve('Forth Promise').then() → .then回调加入微任务队列

4. 第二轮微任务阶段

Promise.resolve('Forth Promise').then() → 输出"Forth Promise"

5. 第三轮宏任务阶段

宏任务队列中的第二个setTimeout
console.log('下下一把再相见'); → 输出"下下一把再相见"

五、 总结

  • Promise构造函数同步执行 : new Promise(executor) 中的executor函数是 同步执行 的
  • 微任务优先级 :所有微任务会在 当前宏任务执行完毕后、下一个宏任务执行前 集中执行
  • 宏任务队列顺序 :按注册顺序执行,每个宏任务执行完毕后会 先清空微任务队列 再继续
  • 嵌套微任务 :宏任务中产生的新微任务会在 当前宏任务执行完毕后立即执行 ,不会等待下一个宏任务
  • 任务类型判断 :
  • Promise.then/catch/finally → 微任务
  • setTimeout/setInterval → 宏任务
  • 同步代码 → 立即执行