理解 JavaScript 中的 Event Loop(事件循环机制)——彻底搞懂异步执行的奥秘!

176 阅读6分钟

什么是Event Loop

event loop(事件循环) 是 JavaScript 中实现异步编程的核心机制,也是 JS 能够在单线程模型下处理大量并发任务的基础。

一句话理解

Event Loop 是协调同步任务与异步任务执行顺序的一套机制。 它负责不断从任务队列中取出任务执行,确保代码按正确的顺序运行。

JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。

宏任务(MacroTask

什么是宏任务(Macro Task)?

宏任务(Macro Task)是事件循环(Event Loop)中的一种任务类型,通常用于调度异步回调。

它代表着整个主线程执行过程中的一个阶段,每次事件循环,宏任务队列中的一个任务会被取出执行,执行完后才会处理所有微任务(Micro Task) ,然后再进入下一轮宏任务。

常见的宏任务包括

  • script全部代码、setTimeoutsetIntervalsetImmediateI/OUI Rendering
  • 理解“宏任务队列”

宏任务会被放入一个任务队列(Task Queue)中,事件循环会从这个队列中每次取出一个宏任务来执行,然后再去清空微任务队列。每次只能执行一个宏任务,因此你可以认为:

每一个宏任务 = 一轮事件循环

什么是微任务(Microtask)?

微任务是 JavaScript 中一种异步任务类型,优先级高于宏任务,用于在当前事件循环结束前执行的短小任务。

它是在一轮事件循环中,当前宏任务执行完之后、下一轮宏任务执行前立即执行的任务。

常见的宏任务包括

Process.nextTick(Node独有)promise.then()Object.observe(废弃)MutationObserverqueueMicrotask()

同步任务和异步任务

Javascript单线程任务被分为同步任务异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

事件循环流程

一个宏任务开始 -> 执行所有同步任务 -> 执行所有微任务 -> 执行下一个宏任务...

如图解释

26489147-02D7-49A9-9589-32DF14940BFE.png

举个例子(process.nextTick PK Promise.then)

console.log('start');
// node 微任务
// process 进程对象
process.nextTick(() => {
    console.log('Process Next Tick');
});
// 微任务
Promise.resolve().then(() => {
    console.log('Promise Resolved');
})
// 宏任务
setTimeout(() => {
    console.log('haha');
    Promise.resolve().then(() => {
        console.log('inner Promise');
    })
}, 0);
console.log('end');

逐步执行流程分析:

✅ Step 1:执行同步代码

console.log('start'); // 输出 1
process.nextTick(...) // 注册 nextTick 回调
Promise.resolve().then(...) // 注册微任务
setTimeout(...) // 注册宏任务
console.log('end'); // 输出 2

此时输出:

start
end

✅ Step 2:执行 process.nextTick()

Process Next Tick

✅ Step 3:执行微任务队列(Promise)

Promise Resolved

✅ Step 4:进入下一轮事件循环 → 执行 setTimeout 宏任务

setTimeout(() => {
  console.log('haha');           // 输出 4
  Promise.resolve().then(() => {
    console.log('inner Promise'); // 注册微任务,输出 5
  })
}, 0);

输出:

haha

✅ Step 5:执行 setTimeout 内的微任务(Promise)

inner Promise

✅ 最终输出顺序:

start
end
Process Next Tick
Promise Resolved
haha
inner Promise

可视化执行顺序:

同步阶段:
  -> log: start
  -> process.nextTick → 注册
  -> Promise.then → 注册
  -> setTimeout → 注册
  -> log: end

微任务阶段:
  -> process.nextTick → 执行
  -> Promise.then → 执行

宏任务阶段(下一轮):
  -> setTimeout → 执行 log: haha
  -> setTimeout 中的 Promise.then → 执行 log: inner Promise

Node.js 环境下

  • process.nextTick()Promise.then() 更早执行
  • 每个宏任务执行完后,都会清空所有微任务队列
  • setTimeout 属于宏任务,要等同步 + 所有微任务跑完后再执行

一个例子不够? 再来一个

console.log('同步Start');

const promise1 = Promise.resolve('Frist 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('下一把再相见');
    const promise4 = Promise.resolve('Forth Promise');
    promise4.then(value => console.log(value));
}, 0);

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

console.log('同步End');

第一轮执行过程详解

▶️ 【同步任务阶段】立即执行

这些代码 立即执行,加入主线程:

console.log('同步Start');     // 输出 ✅
console.log('promise3');       // Promise 执行器是同步的 ✅
console.log('同步End');       // 输出 ✅

此时已输出:

同步Start
promise3
同步End

🕒 微任务阶段(本轮事件循环)

在第一轮 Event Loop 中,执行所有已注册的微任务(即 .then()):

promise1.then(...) // 输出 Frist Promise
promise2.then(...) // 输出 Second Promise
promise3.then(...) // 输出 Third Promise

依注册顺序执行,输出:

Frist Promise
Second Promise
Third Promise

🔁 下一轮事件循环(宏任务阶段)

两个 setTimeout 回调进入宏任务队列,按注册顺序执行:

⏱ 第一个 setTimeout

console.log('下一把再相见');       // 输出
const promise4 = Promise.resolve(...);
promise4.then(...)                 // 注册微任务

然后立刻执行微任务:

console.log('Forth Promise');

⏱ 第二个 setTimeout

console.log('下下一把再相见');

✅ 最终输出顺序总结:

同步Start        // 同步任务
promise3         // Promise 构造函数是同步执行的
同步End

Frist Promise    // 微任务
Second Promise
Third Promise

下一把再相见     // 宏任务 1
Forth Promise     // 宏任务 1 中的微任务

下下一把再相见   // 宏任务 2

📌 最终输出(逐行):

同步Start
promise3
同步End
Frist Promise
Second Promise
Third Promise
下一把再相见
Forth Promise
下下一把再相见

代码一开始执行的是同步任务,所以首先输出的是 同步Start。接着创建了三个 Promise,其中 promise3 是通过 new Promise() 构造函数生成的,在构造函数内部执行了 console.log('promise3'),因为这是同步代码,也会立即输出。然后继续执行同步任务,输出 同步End

同步任务执行完之后,进入微任务阶段。此时之前注册的 promise1.then()promise2.then()promise3.then() 依次被执行,因此会依次输出它们的值:Frist PromiseSecond PromiseThird Promise

微任务清空后,进入下一轮事件循环,也就是宏任务阶段。第一个 setTimeout 执行,输出 下一把再相见,然后在其中又注册了一个新的 Promise,它的 .then() 回调会作为微任务加入当前轮的微任务队列,因此立即执行输出 Forth Promise

接着第二个 setTimeout 被执行,输出 下下一把再相见

总结:彻底搞懂 JavaScript 中的 Event Loop

  • JavaScript 是单线程语言,通过 **Event Loop(事件循环)**机制实现对异步任务的调度。

  • 所有任务分为两类:同步任务异步任务,异步任务又分为:

    • 宏任务(如 setTimeoutsetIntervalI/O 等)
    • 微任务(如 Promise.thenqueueMicrotask
  • 每一轮事件循环会按照以下流程执行:

    1. 执行当前宏任务(包括整体代码 script
    2. 清空微任务队列(执行所有微任务
    3. 开始下一轮宏任务
  • 微任务的执行优先于下一个宏任务,是理解异步执行顺序的关键。

  • 熟练掌握 Event Loop 能帮助我们解决诸如:

    • Promise 链的执行时机
    • 异步 UI 更新
    • setTimeout 与 Promise 谁先执行等面试高频问题
  • 浏览器与 Node.js 的事件循环有差异,但核心机制一致,都是围绕任务队列的调度与执行顺序。

🎯 一句话总结:

弄懂 Event Loop,是成为高级 JavaScript 工程师的必修课。