Node.js 事件循环
Node.js 是一个单线程事件驱动平台,能够运行非阻塞、异步编程。 Node.js 的这些功能使其内存高效。尽管 JavaScript 是单线程的,但事件循环允许 Node.js 执行非阻塞 I/O 操作。它是通过随时随地将操作分配给操作系统来完成的。
大多数操作系统都是多线程的,因此可以处理在后台执行的多个操作。当这些操作之一完成时,内核会告诉 Node.js,分配给该操作的相应回调将添加到最终将被执行的事件队列中。这将在本主题后面进一步详细解释。
事件循环的特点:
- 事件循环是一个无限循环,它等待任务,执行它们然后休眠,直到它收到更多的任务。
- 事件循环仅在调用堆栈为空时执行事件队列中的任务,即没有正在进行的任务。
- 事件循环允许我们使用回调和承诺。
- 事件循环从最旧的开始执行任务。
例子:
console.log("This is the first statement");
setTimeout(function(){
console.log("This is the second statement");
}, 1000);
console.log("This is the third statement");
输出:
This is the first statement
This is the third statement
This is the second statement
说明:在上面的例子中,第一个控制台日志语句被推送到调用堆栈,并且“这是第一条语句”被记录在控制台上,并且任务从堆栈中弹出。接下来,将 setTimeout 推送到队列中,并将任务发送到操作系统,并为任务设置计时器。然后从堆栈中弹出此任务。接下来,将第三条控制台日志语句推送到调用堆栈,并在控制台上记录“这是第三条语句”,并将任务从堆栈中弹出。
当 setTimeout函数设置的计时器(本例中为 1000 毫秒)用完时,将回调发送到事件队列。发现调用堆栈为空的事件循环将位于事件队列顶部的任务发送到调用堆栈。 setTimeout函数的回调函数运行指令,“这是第二条语句”记录在控制台上,任务从堆栈中弹出。
Note: In the above case, if the timeout was set to 0ms then also the statements will be displayed in the same order. This is because although the callback with be immediately sent to the event queue, the event loop won’t send it to the call stack unless the call stack is empty i.e. until the provided input script comes to an end.
事件循环的工作:当 Node.js 启动时,它会初始化事件循环,处理提供的可能进行异步 API 调用的输入脚本,安排计时器,然后开始处理事件循环。在前面的示例中,初始输入脚本由 console.log() 语句和一个调度计时器的 setTimeout()函数组成。
使用 Node.js 时,一个名为 libuv 的特殊库模块用于执行异步操作。该库还与 Node 的后台逻辑一起用于管理一个特殊的线程池,称为 libuv 线程池。该线程池由四个线程组成,用于委托对事件循环来说过于繁重的操作。 I/O 操作、打开和关闭连接、setTimeouts 是此类操作的示例。
当线程池完成一项任务时,会调用一个回调函数来处理错误(如果有)或执行一些其他操作。这个回调函数被发送到事件队列。当调用栈为空时,事件会经过事件队列并将回调发送到调用栈。
下图是 Node.js 服务器中事件循环的正确表示:
事件循环的阶段:下图显示了事件循环操作顺序的简化概述:
- 计时器:由 setTimeout() 或 setInterval() 安排的回调在此阶段执行。
- 待处理回调:延迟到下一个循环迭代的 I/O 回调在此处执行。
- 空闲,准备:仅在内部使用。
- 轮询:检索新的 I/O 事件。
- 检查:它调用 setIntermediate() 回调。
- 关闭回调:它处理一些关闭回调。例如:socket.on('close', ...)