在这里插入图片描述

🤍 前端开发工程师、技术日更博主、已过CET6
🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1
🕠 牛客高级专题作者、打造专栏《前端面试必备》《2024面试高频手撕题》《前端求职突破计划》
🍚 蓝桥云课签约作者、上架课程《Vue.js 和 Egg.js 开发企业级健康管理项目》《带你从入门到实战全面掌握 uni-app》

一、引言

JavaScript 作为一种单线程语言,同一时间只能执行一个任务。然而在实际应用里,诸如网络请求、定时器这类异步操作极为常见。若采用同步方式处理这些操作,会阻塞后续代码的执行,极大地影响程序性能与用户体验。事件循环(Event Loop)机制应运而生,它是 JavaScript 实现异步编程的关键,能让异步任务在不阻塞主线程的情况下得以处理。接下来,我们将深入剖析事件循环的底层原理。

二、JavaScript 运行时环境的基本组成

要理解事件循环,得先了解 JavaScript 运行时环境的基本组成部分,主要包含以下几个方面:

2.1 调用栈(Call Stack)

调用栈是一个后进先出(LIFO)的数据结构,用于存储函数调用信息。当调用一个函数时,该函数会被压入调用栈;函数执行完毕后,会从栈中弹出。例如:

function foo() {
    console.log('Inside foo');
}

function bar() {
    foo();
    console.log('Inside bar');
}

bar();

在这个例子中,调用 bar 函数时,bar 会被压入调用栈;接着 bar 函数内部调用 foo 函数,foo 也会被压入调用栈。foo 函数执行完毕后从栈中弹出,然后 bar 函数继续执行,最后 bar 函数也从栈中弹出。

2.2 任务队列(Task Queue)

任务队列用于存储异步任务的回调函数。当异步任务完成时,其回调函数会被放入任务队列等待执行。任务队列又分为宏任务队列(MacroTask Queue)和微任务队列(MicroTask Queue)。

  • 宏任务:常见的宏任务有 setTimeoutsetIntervalsetImmediate(Node.js 环境)、I/O 操作、UI 渲染等。
  • 微任务:常见的微任务有 Promise.thenMutationObserverprocess.nextTick(Node.js 环境)等。

2.3 堆(Heap)

堆是用于存储对象的内存区域。对象在堆中动态分配内存,变量通过引用指向堆中的对象。

2.4 Web API(浏览器环境)或 Node API(Node.js 环境)

在浏览器环境中,Web API 提供了诸如 setTimeoutfetch 等异步操作的实现。在 Node.js 环境中,Node API 提供了文件系统操作、网络请求等异步功能。

三、事件循环的运行机制

事件循环的核心工作就是不断地从任务队列中取出任务并放入调用栈执行,其具体运行机制如下:

3.1 执行同步代码

JavaScript 引擎首先会执行全局代码中的同步任务,将这些任务依次压入调用栈并执行。同步代码执行过程中,若遇到异步任务,会将其交给相应的 Web API 或 Node API 处理。例如:

console.log('Start');

setTimeout(() => {
    console.log('Timeout callback');
}, 1000);

console.log('End');

在这段代码中,console.log('Start')console.log('End') 是同步任务,会依次执行。而 setTimeout 是异步任务,会交给 Web API 处理。

3.2 异步任务处理

当异步任务被交给 Web API 或 Node API 后,相应的线程会开始处理该任务。例如,setTimeout 会启动一个定时器线程,当定时器时间到达时,其回调函数会被放入任务队列。

3.3 事件循环检查

事件循环会不断检查调用栈是否为空。如果调用栈为空,就会从任务队列中取出任务放入调用栈执行。这里需要注意任务队列的优先级:

  • 微任务队列的优先级高于宏任务队列。也就是说,当调用栈为空时,会先处理微任务队列中的所有任务,直到微任务队列为空,才会从宏任务队列中取出一个任务执行。
  • 每次宏任务执行完毕后,都会检查微任务队列是否有任务,如果有则优先执行微任务队列中的所有任务。

3.4 重复执行

事件循环会不断重复上述步骤,直到任务队列和调用栈都为空。

四、示例代码分析

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

// 宏任务
setTimeout(() => {
    console.log('4. setTimeout 回调执行');
}, 0);

// 微任务
Promise.resolve().then(() => {
    console.log('3. Promise 微任务回调执行');
});

console.log('2. 同步代码结束');

这段代码的执行顺序如下:

  1. 执行同步代码,输出 1. 同步代码开始2. 同步代码结束
  2. 遇到 Promise.resolve().then,将其回调函数放入微任务队列。
  3. 遇到 setTimeout,将其回调函数放入宏任务队列。
  4. 同步代码执行完毕,调用栈为空,开始处理微任务队列,输出 3. Promise 微任务回调执行
  5. 微任务队列处理完毕,开始处理宏任务队列,输出 4. setTimeout 回调执行

五、事件循环在不同环境中的差异

5.1 浏览器环境

在浏览器环境中,事件循环主要处理 DOM 操作、用户交互、网络请求等异步任务。每次事件循环的末尾,浏览器会进行 UI 渲染,确保页面的更新和响应。

5.2 Node.js 环境

Node.js 中的事件循环与浏览器有所不同,它有自己的事件循环模型,包含多个阶段,如 timersI/O callbacksidle, preparepollcheckclose callbacks 等。在 Node.js 中,process.nextTick 会在每个阶段结束时立即执行,优先级高于微任务队列中的其他任务。

六、总结

事件循环是 JavaScript 实现异步编程的核心机制,它通过调用栈、任务队列以及不断的循环检查,使得 JavaScript 能够高效地处理异步任务,避免阻塞主线程。理解事件循环的底层原理,有助于开发者编写更高效、更稳定的 JavaScript 代码,特别是在处理复杂的异步操作时,能更好地控制代码的执行顺序和性能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐