Node.js는 이벤트 루프를 통해 비차단 I/O 작업을 처리하는 단일 스레드 언어입니다. 다음 글은 Node의 이벤트 루프에 대해 자세히 이해하는 데 도움이 되기를 바랍니다.
Node.js JavaScript용 서버측 런타임으로, 브라우저에서 이벤트 루프의 렌더링 단계 없이 주로 네트워크와 파일을 처리합니다.
이벤트 루프 처리 모델을 정의하기 위해 브라우저에 HTML 사양이 있으며, 이는 나중에 다양한 브라우저 제조업체에서 구현됩니다. Node.js의 이벤트 루프 정의와 구현은 Libuv에서 나왔습니다.
Libuv는 이벤트 기반 비동기 I/O 모델을 중심으로 설계되었으며 원래 Node.js용으로 작성되어 크로스 플랫폼 지원 라이브러리를 제공합니다. 아래 그림은 그 구성요소를 보여줍니다. 네트워크 I/O는 네트워크 처리와 관련된 부분이며, 하단에는 epoll, kqueue, 이벤트 포트 및 IOCP가 있습니다. 운영 체제.
Node.js가 시작되면 이벤트 루프를 초기화하고, 제공된 스크립트를 처리하며, 동기 코드가 직접 실행을 위해 스택에 푸시되고, 비동기 작업(네트워크 요청, 파일 작업, 타이밍 API를 호출하고 콜백 함수를 전달한 후 작업은 시스템 커널의 처리를 위해 백그라운드로 전송됩니다. 대부분의 최신 커널은 작업 중 하나가 완료되면 콜백 함수를 폴링 대기열에 추가하고 실행 기회를 기다리도록 Node.js에 알립니다.
아래 사진의 왼쪽은 Node.js 공식 홈페이지의 이벤트 루프 프로세스에 대한 설명이고, 오른쪽은 Libuv 공식 홈페이지의 Node.js에 대한 설명입니다. 둘 다 이벤트 루프에 대한 소개입니다. .모든 사람이 소스 코드를 볼 수 있는 것은 아닙니다. 일반적으로 두 문서는 이벤트 루프에 대한 학습을 위한 보다 직접적인 참조 문서입니다. Node.js 공식 웹사이트의 소개는 매우 자세하며 학습을 위한 참고 자료로 사용할 수 있습니다.
왼쪽 Node.js 공식 웹사이트에 표시된 이벤트 루프는 6단계로 구분됩니다. 각 단계에는 콜백 기능을 실행하는 FIFO(선입 선출) 대기열이 있습니다. 이 단계는 아직 명확합니다.
오른쪽에 이벤트 루프를 반복하기 전에 먼저 루프가 활성 상태인지 확인합니다(대기 중인 비동기 I/O, 타이머 등이 있음). 반복을 시작합니다. 그렇지 않으면 루프가 즉시 종료됩니다.
각 단계에 대해서는 아래에서 별도로 논의하겠습니다.
먼저 이벤트 루프는 두 개의 API setTimeout(cb, ms) 및 setInterval(cb, ms)을 포함하는 타이머 단계에 들어갑니다. 전자는 한 번만 실행되고 후자는 반복됩니다. 구현하다.
이 단계에서는 만료된 타이머 함수가 있는지 확인합니다. 만료된 타이머 콜백 함수는 브라우저에서와 마찬가지로 타이머 함수에 의해 전달되는 지연 시간이 항상 예상보다 늦습니다. 운영 체제나 기타 실행 중인 콜백 함수의 영향을 받습니다.
예를 들어 다음 예에서는 타이머 기능을 설정하고 1000밀리초 후에 실행될 것으로 예상합니다.
const now = Date.now(); setTimeout(function timer1(){ log(`delay ${Date.now() - now} ms`); }, 1000); setTimeout(function timer2(){ log(`delay ${Date.now() - now} ms`); }, 5000); someOperation(); function someOperation() { // sync operation... while (Date.now() - now < 3000) {} }
setTimeout 비동기 함수를 호출한 후 프로그램은 someOperation() 함수를 실행합니다. 중간에 시간이 많이 걸리는 일부 작업은 이러한 동기 작업을 완료한 후 이벤트 루프에 들어가 먼저 타이머 단계를 확인합니다. 태스크, 타이머 스크립트는 지연 시간의 오름차순으로 힙 메모리에 저장됩니다. 먼저 타임아웃 시간이 가장 작은 타이머 함수를 꺼내어 확인합니다. 그렇지 않은 경우 계속 확인합니다. 만료되지 않은 타이머 기능에 도달하거나 최대 시스템 종속성에 도달한 후 다음 단계로 이동합니다. 이 예에서는 someOperation() 함수를 실행한 후 현재 시간이 T + 3000이라고 가정합니다.
timer1 함수를 확인하면 현재 시간은 T + 3000 - T > 1000으로 예상 지연 시간을 초과합니다. 콜백 함수 실행이 끝나면 계속 확인하세요.
timer2 기능을 확인하세요. 현재 시간은 T + 3000 - T
pending callbacksidle, prepare 阶段是给系统内部使用,idle 这个名字很迷惑,尽管叫空闲,但是在每次的事件循环中都会被调用,当它们处于活动状态时。这一块的资料介绍也不是很多。略...
poll 是一个重要的阶段,这里有一个概念观察者,有文件 I/O 观察者,网络 I/O 观察者等,它会观察是否有新的请求进入,包含读取文件等待响应,等待新的 socket 请求,这个阶段在某些情况下是会阻塞的。
在阻塞 I/O 之前,要计算它应该阻塞多长时间,参考 Libuv 文档上的一些描述,以下这些是它计算超时时间的规则:
如果循环使用 UV_RUN_NOWAIT 标志运行、超时为 0。
如果循环将要停止(uv_stop() 被调用),超时为 0。
如果没有活动的 handlers 或 request,超时为 0。
如果有任何 idle handlers 处于活动状态,超时为 0。
如果有任何待关闭的 handlers,超时为 0。
如果以上情况都没有,则采用最近定时器的超时时间,或者如果没有活动的定时器,则超时时间为无穷大,poll 阶段会一直阻塞下去。
很简单的一段代码,我们启动一个 Server,现在事件循环的其它阶段没有要处理的任务,它会在这里等待下去,直到有新的请求进来。
const http = require('http'); const server = http.createServer(); server.on('request', req => { console.log(req.url); }) server.listen(3000);
结合阶段一的定时器,在看个示例,首先启动 app.js 做为服务端,模拟延迟 3000ms 响应,这个只是为了配合测试。再运行 client.js 看下事件循环的执行过程:
首先程序调用了一个在 1000ms 后超时的定时器。
之后调用异步函数 someAsyncOperation() 从网络读取数据,我们假设这个异步网路读取需要 3000ms。
当事件循环开始时先进入 timer 阶段,发现没有超时的定时器函数,继续向下执行。
期间经过 pending callbacks -> idle,prepare 当进入 poll 阶段,此时的 http.get() 尚未完成,它的队列为空,参考上面 poll 阻塞超时时间规则,事件循环机制会检查最快到达阀值的计时器,而不是一直在这里等待下去。
当大约过了 1000ms 后,进入下一次事件循环进入定时器,执行到期的定时器回调函数,我们会看到日志 setTimeout run after 1003 ms。
在定时器阶段结束之后,会再次进入 poll 阶段,继续等待。
// client.js const now = Date.now(); setTimeout(() => log(`setTimeout run after ${Date.now() - now} ms`), 1000); someAsyncOperation(); function someAsyncOperation() { http.get('http://localhost:3000/api/news', () => { log(`fetch data success after ${Date.now() - now} ms`); }); } // app.js const http = require('http'); http.createServer((req, res) => { setTimeout(() => { res.end('OK!') }, 3000); }).listen(3000);
当 poll 阶段队列为空时,并且脚本被 setImmediate() 调度过,此时,事件循环也会结束 poll 阶段,进入下一个阶段 check。
check 阶段在 poll 阶段之后运行,这个阶段包含一个 API setImmediate(cb) 如果有被 setImmediate 触发的回调函数,就取出执行,直到队列为空或达到系统的最大限制。
拿 setTimeout 和 setImmediate 对比,这是一个常见的例子,基于被调用的时机和定时器可能会受到计算机上其它正在运行的应用程序影响,它们的输出顺序,不总是固定的。
setTimeout(() => log('setTimeout')); setImmediate(() => log('setImmediate')); // 第一次运行 setTimeout setImmediate // 第二次运行 setImmediate setTimeout
但是一旦把这两个函数放入一个 I/O 循环内调用,setImmediate 将总是会被优先调用。因为 setImmediate 属于 check 阶段,在事件循环中总是在 poll 阶段结束后运行,这个顺序是确定的。
fs.readFile(__filename, () => { setTimeout(() => log('setTimeout')); setImmediate(() => log('setImmediate')); })
在 Libuv 中,如果调用关闭句柄 uv_close(),它将调用关闭回调,也就是事件循环的最后一个阶段 close callbacks。
这个阶段的工作更像是做一些清理工作,例如,当调用 socket.destroy(),'close' 事件将在这个阶段发出,事件循环在执行完这个阶段队列里的回调函数后,检查循环是否还 alive,如果为 no 退出,否则继续下一次新的事件循环。
包含 Microtask 的事件循环流程图
在浏览器的事件循环中,把任务划分为 Task、Microtask,在 Node.js 中是按照阶段划分的,上面我们介绍了 Node.js 事件循环的 6 个阶段,给用户使用的主要是 timer、poll、check、close callback 四个阶段,剩下两个由系统内部调度。这些阶段所产生的任务,我们可以看做 Task 任务源,也就是常说的 “Macrotask 宏任务”。
通常我们在谈论一个事件循环时还会包含 Microtask,Node.js 里的微任务有 Promise、还有一个也许很少关注的函数 queueMicrotask,它是在 Node.js v11.0.0 之后被实现的,参见 PR/22951。
Node.js 中的事件循环在每一个阶段执行后,都会检查微任务队列中是否有待执行的任务。
Node.js 在 v11.x 前后,每个阶段如果即存在可执行的 Task 又存在 Microtask 时,会有一些差异,先看一段代码:
setImmediate(() => { log('setImmediate1'); Promise.resolve('Promise microtask 1') .then(log); }); setImmediate(() => { log('setImmediate2'); Promise.resolve('Promise microtask 2') .then(log); });
在 Node.js v11.x 之前,当前阶段如果存在多个可执行的 Task,先执行完毕,再开始执行微任务。基于 v10.22.1 版本运行结果如下:
setImmediate1 setImmediate2 Promise microtask 1 Promise microtask 2
在 Node.js v11.x 之后,当前阶段如果存在多个可执行的 Task,先取出一个 Task 执行,并清空对应的微任务队列,再次取出下一个可执行的任务,继续执行。基于 v14.15.0 版本运行结果如下:
setImmediate1 Promise microtask 1 setImmediate2 Promise microtask 2
在 Node.js v11.x 之前的这个执行顺序问题,被认为是一个应该要修复的 Bug 在 v11.x 之后并修改了它的执行时机,和浏览器保持了一致,详细参见 issues/22257 讨论。
Node.js 中还有一个异步函数 process.nextTick(),从技术上讲它不是事件循环的一部分,它在当前操作完成后处理。如果出现递归的 process.nextTick() 调用,这将会很糟糕,它会阻断事件循环。
如下例所示,展示了一个 process.nextTick() 递归调用示例,目前事件循环位于 I/O 循环内,当同步代码执行完成后 process.nextTick() 会被立即执行,它会陷入无限循环中,与同步的递归不同的是,它不会触碰 v8 最大调用堆栈限制。但是会破坏事件循环调度,setTimeout 将永远得不到执行。
fs.readFile(__filename, () => { process.nextTick(() => { log('nextTick'); run(); function run() { process.nextTick(() => run()); } }); log('sync run'); setTimeout(() => log('setTimeout')); }); // 输出 sync run nextTick
将 process.nextTick 改为 setImmediate 虽然是递归的,但它不会影响事件循环调度,setTimeout 在下一次事件循环中被执行。
fs.readFile(__filename, () => { process.nextTick(() => { log('nextTick'); run(); function run() { setImmediate(() => run()); } }); log('sync run'); setTimeout(() => log('setTimeout')); }); // 输出 sync run nextTick setTimeout
process.nextTick 是立即执行,setImmediate 是在下一次事件循环的 check 阶段执行。但是,它们的名字着实让人费解,也许会想这两个名字交换下比较好,但它属于遗留问题,也不太可能会改变,因为这会破坏 NPM 上大部分的软件包。
在 Node.js 的文档中也建议开发者尽可能的使用 setImmediate(),也更容易理解。
更多node相关知识,请访问:nodejs 教程!!
위 내용은 Node의 이벤트 루프에 대해 자세히 알아보는 기사의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!