JavaScript的事件循环(EventLoop)机制

84 阅读8分钟

JavaScript 的事件循环(Event Loop)是 JavaScript 实现异步编程的核心机制,它决定了代码的执行顺序,尤其是在处理异步任务(如网络请求、定时器、DOM 事件等)时。

理解事件循环是掌握 JavaScript 异步编程的关键。

JavaScript运行机制

JavaScript引擎在执行JavaScript代码时,会将任务分为两类:同步任务异步任务。同步任务在主线程上执行,而异步任务则由任务队列中的事件循环机制异步执行。在异步任务完成后,就会将该任务对应的回调函数放入任务队列中,并等待主线程执行完当前所有的同步任务后再执行该回调函数。

JavaScript中的回调函数是一种特殊的函数,它作为另一个函数的参数传递进去,在该函数执行完特定的操作后被异步调用。

事件循环的核心概念

JavaScript 是单线程的(只有一个主线程执行代码),但浏览器或 Node.js 环境提供了多线程的异步 API(如网络线程、定时器线程等)。事件循环的作用是协调主线程与异步任务的执行顺序。

JavaScript 是单线程的,意味着同一时间只能做一件事情,但是并不意味着单线程就是阻塞,实现单线程非阻塞的方法就是事件循环。

核心组成部分:

  1. 调用栈(Call Stack) :执行同步代码的地方,遵循 “后进先出”(LIFO)原则。
  2. 回调队列(Callback Queue) :存放异步任务的回调函数(如 setTimeoutfetch 的回调),遵循 “先进先出”(FIFO)原则。
  3. 微任务队列(Microtasks Queue) :优先级高于回调队列,存放 Promise 回调(then/catch/finally)、queueMicrotask 等。
  4. 宏任务队列(Macrotasks Queue) :即传统的回调队列,存放 setTimeoutsetInterval、DOM 事件、fetch 响应等。

事件循环的执行流程

  1. 执行同步代码:主线程从调用栈中依次执行同步代码,遇到异步任务时,将其交给对应的心搏线程处理(如定时器线程处理 setTimeout)。
  2. 处理异步任务:异步任务完成后,其回调函数会被放入对应的队列(微任务放微任务队列,宏任务放宏任务队列)。
  3. 执行微任务:当调用栈为空时,立即执行微任务队列中的所有任务(按顺序执行,直到清空)。
  4. 执行宏任务:微任务队列清空后,从宏任务队列中取第一个任务放入调用栈执行。
  5. 循环往复:重复步骤 3-4,形成事件循环。

流程图示意

同步代码 → 调用栈清空 → 执行所有微任务 → 执行一个宏任务 → 调用栈清空 → ...(循环)

微任务与宏任务的分类

微任务(优先级高)

  • Promise 的 then/catch/finally 回调
  • queueMicrotask 函数
  • process.nextTick(Node.js 环境,优先级高于其他微任务)
  • MutationObserver(监听 DOM 变化的回调)

执行机制:微任务队列在当前宏任务执行完毕后,会被立即执行,在微任务队列中的所有任务都执行完之前,不会执行下一个宏任务。

宏任务(优先级低)

  • setTimeout/setInterval
  • DOM 事件(如 clickscroll
  • fetch 等网络请求的响应回调
  • setImmediate(Node.js 环境)
  • I/O 操作(如文件读写,Node.js 环境)
  • postMessageMessageChanner

执行机制:在事件循环中,每次取出一个宏任务执行,执行完毕后,会去检查微任务队列,将微任务队列中的所有微任务执行完毕后,再取下一个宏任务执行。

事件循环(Event Loop)与宏任务、微任务的关系

  1. 执行栈执行同步代码,遇到异步任务时,根据类型将其放入对应的宏任务队列或微任务队列。
  2. 当执行栈为空时,先检查微任务队列。如果有微任务,依次执行所有微任务,直到微任务队列为空。
  3. 微任务队列执行完毕后,从宏任务队列中取出一个宏任务执行。
  4. 执行完这个宏任务后,再次检查微任务队列,重复上述过程,形成事件循环。

关键特性总结

  1. 单线程执行:同步代码依次执行,异步任务不阻塞主线程。
  2. 微任务优先:每次调用栈清空后,会先执行完所有微任务,再执行一个宏任务。
  3. 队列顺序:同一队列中的任务按 “先进先出” 执行。
  4. DOM 渲染时机:在微任务执行完毕、下一个宏任务执行前,浏览器可能会进行 DOM 渲染(这也是为什么微任务中修改 DOM 会同步反映)。

经典示例解析

通过代码示例理解执行顺序:

示例解析1

console.log('1'); // 同步代码

setTimeout(() => {
  console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // 微任务
}).then(() => {
  console.log('4'); // 微任务
});

console.log('5'); // 同步代码

执行步骤

  1. 执行同步代码 console.log('1') → 输出 1
  2. 遇到定时器setTimeout,将回调放入宏任务队列,后面执行 → 宏任务队列:[() => console.log('2')]
  3. 遇到 Promise.resolve().then(),将第一个 then 回调放入微任务队列 → 微任务队列:[() => console.log('3')]
  4. 执行同步代码 console.log('5') → 输出 5
  5. 调用栈清空,执行所有微任务:
  6. 执行第一个微任务 → 输出 3,此时第二个 then 回调进入微任务队列 → 微任务队列:[() => console.log('4')]
  7. 继续执行微任务 → 输出 4,微任务队列清空。
  8. 从宏任务队列取第一个任务执行 → 输出 2最终输出顺序1 → 5 → 3 → 4 → 2

示例解析2

console.log(1)// 同步代码

setTimeout(()=>{
    console.log(2)// 宏任务
}, 0)

new Promise((resolve, reject)=>{
    console.log('new Promise')// 同步代码
    resolve()
}).then(()=>{
    console.log('then')// 微任务
})
console.log(3)// 同步代码

执行步骤

// 遇到同步代码 console.log (1) ,直接打印 1  
// 遇到定时器,属于新的宏任务,留着后面执行  
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'  
//.then 属于微任务,放入微任务队列,后面再执行  
// 遇到同步代码 console.log (3) 直接打印 3  
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'  
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2

最终结果:1->'new Promise'->3->'then'->2

由此可见,事件循环、微任务、宏任务关系,如下图:

image.png

常见面试题场景

  • setTimeout 延迟不准:因为 setTimeout 回调需要等待调用栈清空和前面的任务执行完,实际延迟可能大于设定值。
  • Promise 与 setTimeout 优先级:Promise 回调(微任务)总是比 setTimeout(宏任务)先执行。
  • 嵌套异步任务:内部的微任务仍会在当前宏任务执行完前优先执行。

async与await

async 是异步的意思,await 则可以理解为 async wait。所以可以理解 async 就是用来声明一个异步方法,而 await 是用来等待异步方法执行

async

async 函数返回一个 promise 对象,下面两种方法是等效的

function f() {
    return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}

await

正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

async function f(){
    // 等同于
    // return 123
    return await 123
}
f().then(v => console.log(v)) // 123

不管 await 后面跟着的是什么,await 都会阻塞后面的代码

流程分析

script start -> async1 start -> async2 -> promise1 -> script end -> 
同步代码执行完成
执行加入到微任务队列中的微任务代码 -> async1 end 
上一个微任务执行完成,开始下一个微任务 -> promise2
上一个宏任务都执行完成,开始下一个宏任务 ->settimeout

async function async1() {
    console.log('async1 start')// 同步代码
    await async2()
    console.log('async1 end') // 阻塞 ,加入到微任务队列,跳出去执行同步代码
}

async function async2() {
    console.log('async2')// 同步代码
}

console.log('script start') // 同步代码

setTimeout(function () {
    console.log('settimeout')// 宏任务,先不执行,等微任务都执行完成后才执行
})

async1()

new Promise(function (resolve) {
    console.log('promise1')// 同步代码
    resolve()
}).then(function () {
    console.log('promise2')// 微任务,加入到微任务队列
})

console.log('script end')// 同步代码

分析过程:

  1. 执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
  2. 遇到定时器了,它是宏任务,先放着不执行
  3. 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到 await 怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
  4. 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
  5. 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await 下面的代码,打印 async1 end
  6. 继续执行下一个微任务,即执行 then 的回调,打印 promise2
  7. 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout

所以最后的结果是:script startasync1 startasync2promise1script endasync1 endpromise2settimeout

参考文档:juejin.cn/post/724289…