Skip to content

Event Loop

单线程

JavaScript 是单线程的编程语言,所有的代码片段都会在堆栈中进行,同一时间只能执行一个代码片段。

堆栈

当我们运行一段 JavaScript 代码时,JavaScript 引擎会创建一个全局运行环境进入到堆栈中,从上往下逐行运行代码。

当遇见函数调用时,被调用的函数加入到堆栈中。当函数运行到函数体结尾或者遇见 return 语句时,该函数就会从堆栈中弹出。

堆栈的限制

浏览器对堆栈会设置有最大次数,避免无限次数的函数调用导致程序崩溃。

javascript
// 这段代码导致了无限循环,浏览器会在一定次数后中止掉程序,并弹出错误。
function foo() {
  return foo();
}

foo();

同步代码造成的阻塞

假设 async 都是同步代码,必须等到上一个任务完成,才能执行下一行代码,那么就会导致阻塞的发生,从而导致页面无法响应用户的请求,破坏了用户体验。

javascript
syncTask1();
syncTask2();
syncTask3():

异步代码和事件循环

因为长时间的同步等待会导致阻塞问题,因此我们在开发应用程序时以异步任务为主。

当 JavaScript 引擎执行完工作区的同步代码后,就会检查任务队列(Task Queue),如果队列有任务,则取出来加入到堆栈中运行,这就是最基本的事件循环模型。

任务类型(Task Type)

可以参考任务

Event Loop 在浏览器环境与 Node.js 环境的区别

  1. 上下文对象的不同

在浏览器中,每个标签页有自己的 JavaScript 执行环境,因此每个标签页都有自己的上下文对象(window)和事件循环模型。

在 Node.js 中,所有的 JavaScript 代码都运行在一个共享的运行时环境中,即 Node.js 进程。因此,Node.js 中没有独立的上下文对象,Node.js 中的全局上下文对象是 global

  1. 事件源的不同

在浏览器中,事件循环主要用于处理 DOM 事件,是鼠标点击事件,网络请求完成,定时器到期等。

在 Node.js 中,事件循环主要用于处理文件 I/O 操作,网络通信,数据库操作和进程级别的事件等。

  1. 全局对象的不同

在浏览器中,全局对象是 window

在 Node.js 中,全局对象是 global

Node.js 中的 Event Loop

在 Node.js 中,Event Loop 可以分为 6 个阶段:

  1. timer 阶段
  • 这个阶段用于执行 setTimeout()setInterval() 方法的回调函数
  • Node.js 会检查定时器是否超时,如果超时,则会运行对应的回调函数
  • 执行顺序取决于设定的超时时间,先超时的定时器回调函数会先执行
  1. I/O Callbacks 阶段
  • 这个阶段处理系统 I/O 操作(如文件系统,网络请求)产生的回调函数
  • 当 I/O 操作完成时,相应的回调函数会在这个阶段执行
  1. idle、prepare 阶段
  • 这两个阶段通常不被开发者频繁使用,处于准备和空闲状态时执行一些特殊的回调函数。
  1. poll 阶段(轮询阶段):
  • 该阶段用于等待新的事件,并处理 I/O 回调函数。
  • 如果没有定时器和 I/O 回调函数需要处理,Node.js 将在这个阶段等待事件(例如新的请求、连接等)。
  • 当有新事件到达或者超时的定时器回调需要执行时,Event Loop 将跳出 poll 阶段,进入下一个阶段。
  1. check 阶段(检查阶段):
  • 该阶段用于执行 setImmediate() 函数产生的回调函数。
  • setImmediate() 的回调函数会在这个阶段执行,但是在 poll 阶段之后。
  1. close callbacks 阶段(关闭回调阶段):
  • 该阶段处理一些关闭的回调函数,例如 socket.on('close', ...)。 在套接字或句柄关闭时,相关的回调函数会在这个阶段执行。

Node.js 的 Event Loop 一直在这些阶段之间循环执行,处理各种异步任务,直到没有更多的任务需要执行。这个循环使得 Node.js 能够高效地处理大量的并发请求和异步操作,从而实现非阻塞的事件驱动编程。开发者可以利用这些不同的阶段来管理和优化应用程序的性能和行为。