🧠JavaScript 事件循环(Event Loop)详解

104 阅读4分钟

前言

JavaScript 是一种单线程语言,这意味着它一次只能执行一个任务。这种设计简化了开发过程,避免了多线程编程中的复杂性,但也带来了问题——如何在不阻塞主线程的情况下处理耗时操作(如网络请求、定时器等)。事件循环(Event Loop) 正是 JavaScript 用来解决这个问题的核心机制。

一、事件循环的基本流程

image.png

事件循环是 JavaScript 引擎处理异步任务的核心机制,确保了程序的流畅执行和响应性。根据流程图,事件循环可以分为以下几个步骤:

  1. 宏任务执行

    • 每次事件循环从一个宏任务开始。
    • 宏任务包括:<script> 脚本、setTimeoutsetInterval、用户交互事件等。
  2. 宏任务执行完毕

    • 当前宏任务中的所有同步代码执行完毕后,进入下一步。
  3. 检查是否有微任务

    • 在宏任务执行完毕后,会检查是否存在需要执行的微任务。
    • 微任务包括:Promise.then()MutationObserverqueueMicrotask 等。
  4. 执行所有微任务

    • 如果存在微任务,则依次执行所有排队的微任务。
    • 微任务会在当前宏任务结束后立即执行,但在此之前不会进行页面渲染
  5. 渲染

    • 所有微任务执行完毕后,浏览器会进行页面渲染,更新 DOM 和样式,重排重绘等。
    • 渲染完成后,事件循环会继续寻找下一个宏任务,开启下一次循环。

二、示例分析

让我们通过一个具体的例子来更好地理解这个流程:

console.log('同步Start')

// 同步任务 .then()才是微任务
const promise1 = Promise.resolve('First Promise')
const promise2 = Promise.resolve('Second Promise')
const promise3 = new Promise(resolve => {
    console.log('Promise3') 
    resolve('Third Promise')
})

// 异步任务 宏任务
setTimeout(() => {
    console.log('下一把再见')
    const promise4 = Promise.resolve('Fourth Promise')
    promise4.then(res => {
        console.log(res)
    })
}, 0)

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

// 异步任务 微任务
promise1.then(res => {
    console.log(res)
})
promise2.then(res => {
    console.log(res)
})
promise3.then(res => {
    console.log(res)
})

console.log('同步End')

🔍 执行流程分析

JavaScript 是单线程的,它使用 调用栈(Call Stack) 来管理函数执行,并通过 事件循环机制 协调异步操作。

调用栈 详见文章 juejin.cn/post/751054…

执行顺序规则如下:

  1. 执行当前宏任务中的所有同步代码;
  2. 执行所有已排队的微任务(直到队列为空);
  3. 渲染页面(可选,浏览器行为);
  4. 开始下一轮事件循环,从宏任务队列中取出下一个任务。

 详细执行步骤

1. 第一个宏任务开始(脚本整体)

console.log('同步Start')

输出:同步Start

创建并立即执行三个 Promise(同步代码)
const promise1 = Promise.resolve('First Promise') // 同步执行
const promise2 = Promise.resolve('Second Promise') // 同步执行
const promise3 = new Promise(resolve => {
    console.log('Promise3') 
    resolve('Third Promise')
})

输出:Promise3

注意:Promise.resolve()new Promise() 中传入的函数是同步执行的,.then() 是异步的(属于微任务)。

注册两个 setTimeout 宏任务
setTimeout(() => { ... }, 0) // 宏任务1
setTimeout(() => { ... }, 0) // 宏任务2

这两个定时器被加入宏任务队列,等待当前宏任务结束后执行。

 注册三个 .then() 微任务
promise1.then(...) // 微任务1
promise2.then(...) // 微任务2
promise3.then(...) // 微任务3

这些回调被加入微任务队列,将在当前宏任务完成后立即执行。

继续执行同步代码
console.log('同步End')

输出:同步End

此时当前宏任务完成

2. 当前宏任务结束 → 执行所有微任务

微任务队列中有三个 .then() 回调,依次执行:

promise1.then(res => console.log(res)) // First Promise
promise2.then(res => console.log(res)) // Second Promise
promise3.then(res => console.log(res)) // Third Promise

输出:

First Promise
Second Promise
Third Promise

微任务队列清空完毕。

3. 页面渲染(可选,浏览器行为)

4. 下一轮事件循环 → 执行宏任务队列中的第一个宏任务

setTimeout(() => {
    console.log('下一把再见')
    const promise4 = Promise.resolve('Fourth Promise')
    promise4.then(res => {
        console.log(res)
    })
}, 0)

输出:

下一把再见

同时注册了一个新的微任务(promise4.then(...)),这个微任务会在本轮宏任务结束后立即执行。

5. 执行该宏任务后产生的微任务

promise4.then(res => console.log(res))

输出:

Fourth Promise

6. 再次进入事件循环 → 执行第二个宏任务

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

输出:

下下把再见

最终完整输出顺序:

image.png

三、为什么要把渲染放在宏任务前、微任务后?

  • 批量更新处理,减少重绘重排:将渲染放在宏任务前、微任务后,可以确保所有 JavaScript(同步和微任务)执行完毕,DOM 更新完成且数据一致,从而减少页面重绘次数,提升性能。
  • 保证最终结果:微任务机制让开发者能在渲染前对 DOM 做最后调整,确保用户看到的是最终结果。

四、总结

事件循环机制是 JavaScript 异步编程的基础,理解其工作原理有助于我们编写更高效、更稳定的代码。通过合理利用宏任务和微任务,我们可以优化程序的执行流程,提升用户体验。