>  기사  >  웹 프론트엔드  >  JS 및 Node.js의 이벤트 루프에 대한 자세한 설명

JS 및 Node.js의 이벤트 루프에 대한 자세한 설명

小云云
小云云원래의
2017-12-13 09:30:471441검색

js의 이벤트 루프는 Chrome과 노드에서 setTimeoutPromise를 사용하여 프로그램을 실행할 때 실행 결과가 다른 문제로 이어집니다. 따라서 이 문서는 Nodejs의 이벤트 루프 메커니즘을 소개합니다. JS와 Node.js의 이벤트 원리와 사용법을 예제를 통해 자세히 분석하는 것이 도움이 되기를 바랍니다. event loop,引出了chrome与node中运行具有setTimeoutPromise的程序时候执行结果不一样的问题,从而引出了Nodejs的event loop机制,本篇文章通过实例给大家详细分析了JS与Node.js中的事件的原理以及用法,希望能帮助到大家。

console.log(1)
setTimeout(function() {
 new Promise(function(resolve, reject) {
 console.log(2)
 resolve()
 })
 .then(() => {
 console.log(3)
 })
}, 0)
setTimeout(function() {
 console.log(4)
}, 0)
// chrome中运行:1 2 3 4
// Node中运行: 1 2 4 3

chrome和Node执行的结果不一样,这就很有意思了。

1. JS 中的任务队列

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

2. 任务队列 event loop

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。主线程不断重复上面的第三步。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

3. 定时器 setTimeoutsetInterval

定时器功能主要由setTimeout()setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()

console.log('1')
setTimeout(function() {
 console.log('2')
 new Promise(function(resolve) {
 console.log('4')
 resolve()
 }).then(function() {
 console.log('5')
 })
 setTimeout(() => {
 console.log('haha')
 })
 new Promise(function(resolve) {
 console.log('6')
 resolve()
 }).then(function() {
 console.log('66')
 })
})
setTimeout(function() {
 console.log('hehe')
}, 0)
new Promise(function(resolve) {
 console.log('7')
 resolve()
}).then(function() {
 console.log('8')
})
setTimeout(function() {
 console.log('9')
 new Promise(function(resolve) {
 console.log('11')
 resolve()
 }).then(function() {
 console.log('12')
 })
})
new Promise(function(resolve) {
 console.log('13')
 resolve()
}).then(function() {
 console.log('14')
})
// node1 : 1,7,13,8,14,2,4,6,hehe,9,11,5,66,12,haha // 结果不稳定
// node2 : 1,7,13,8,14,2,4,6,hehe,5,66,9,11,12,haha // 结果不稳定
// node3 : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha // 结果不稳定
// chrome : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha

Chrome과 Node의 실행 결과가 다르다는 점이 매우 흥미롭습니다. 1. JS의 작업 대기열

🎜🎜🎜JavaScript 언어의 주요 기능은 단일 스레딩, 즉 동시에 한 가지 작업만 수행할 수 있다는 것입니다. 그렇다면 JavaScript는 왜 다중 스레드를 가질 수 없습니까? 이렇게 하면 효율성이 향상될 수 있습니다.
🎜🎜JavaScript의 단일 스레드는 그 목적과 관련이 있습니다. 브라우저 스크립팅 언어인 JavaScript의 주요 목적은 사용자와 상호 작용하고 DOM을 조작하는 것입니다. 이는 단일 스레드만 가능하다는 것을 결정합니다. 그렇지 않으면 매우 복잡한 동기화 문제가 발생합니다. 예를 들어 JavaScript에 두 개의 스레드가 동시에 있다고 가정해 보겠습니다. 한 스레드는 특정 DOM 노드에 콘텐츠를 추가하고 다른 스레드는 해당 노드를 삭제합니다. 이 경우 브라우저는 어떤 스레드를 사용해야 합니까?
🎜🎜복잡함을 피하기 위해 JavaScript는 탄생부터 단일 스레드로 구성되었으며 이는 이 언어의 핵심 기능이 되었으며 앞으로도 변하지 않을 것입니다.
🎜🎜멀티 코어 CPU의 컴퓨팅 성능을 활용하기 위해 HTML5는 JavaScript 스크립트가 여러 스레드를 생성할 수 있도록 허용하는 Web Worker 표준을 제안하지만 하위 스레드는 기본 스레드에 의해 완전히 제어되며 반드시 DOM을 운영하지 마세요. 따라서 이 새로운 표준은 JavaScript의 단일 스레드 특성을 변경하지 않습니다. 🎜🎜🎜🎜2. 작업 대기열 이벤트 루프🎜🎜🎜🎜단일 스레드는 모든 작업을 대기열에 넣어야 하며 다음 작업이 실행되기 전에 이전 작업이 완료된다는 의미입니다. 이전 작업이 오래 걸리면 다음 작업은 기다려야 합니다.
🎜🎜그래서 모든 작업은 두 가지 유형으로 나눌 수 있는데, 하나는 동기식 작업(동기식)이고 다른 하나는 비동기식 작업(비동기식)입니다. 동기 작업은 메인 스레드에서 실행을 위해 대기 중인 작업을 의미합니다. 다음 작업은 이전 작업이 실행된 후에만 실행될 수 있습니다. "작업 대기열"이 비동기 작업이 실행될 수 있음을 메인 스레드에 알리면 작업이 실행을 위해 메인 스레드에 들어가게 됩니다.
🎜🎜구체적으로 비동기 실행의 동작 메커니즘은 다음과 같습니다. (동기 실행도 마찬가지입니다. 비동기 작업 없이 비동기 실행으로 간주될 수 있기 때문입니다.) 🎜🎜모든 동기 작업은 메인 스레드에서 실행되어 실행 컨텍스트 스택을 형성합니다. 메인 스레드 외에 "작업 대기열"도 있습니다. 비동기 작업에 실행 결과가 있는 한 이벤트는 "작업 대기열"에 배치됩니다. "실행 스택"의 모든 동기화 작업이 실행되면 시스템은 "작업 대기열"을 읽어 그 안에 어떤 이벤트가 있는지 확인합니다. 해당 비동기 작업은 대기 상태를 종료하고 실행 스택에 들어가 실행을 시작합니다. 메인 스레드는 위의 세 번째 단계를 계속 반복합니다. 🎜

🎜🎜메인 스레드가 비어 있는 한 "작업 대기열"을 읽습니다. 이것이 JavaScript의 실행 메커니즘입니다. 이 과정이 계속 반복됩니다. 🎜🎜🎜🎜3.Timer🎜🎜 setTimeoutsetInterval🎜🎜타이머 함수는 주로 setTimeout()setInterval로 구성됩니다. ( )이 두 기능을 완성하기 위한 내부 작동 메커니즘은 전자가 지정한 코드가 한 번 실행되고 후자는 반복적으로 실행된다는 점만 다릅니다. 🎜🎜setTimeout(fn,0)은 메인 스레드의 사용 가능한 가장 빠른 유휴 시간에 실행될 작업, 즉 최대한 빨리 실행되도록 지정하는 것을 의미합니다. "작업 대기열" 끝에 이벤트를 추가하므로 동기화 작업과 "작업 대기열"의 기존 이벤트가 처리될 때까지 실행되지 않습니다. 🎜🎜HTML5 표준은 setTimeout()의 두 번째 매개변수의 최소값(최단 간격)을 지정하며, 이 값은 4보다 작을 수 없습니다. 이 값보다 작으면 자동으로 증가합니다. 이전 버전의 브라우저에서는 최소 간격을 10밀리초로 설정했습니다. 또한 이러한 DOM 변경(특히 페이지 재렌더링과 관련된 변경)은 일반적으로 즉시 실행되지 않고 16밀리초마다 실행됩니다. 이때 setTimeout()보다 requestAnimationFrame()을 사용하는 것이 효과가 더 좋습니다. 🎜🎜setTimeout()은 이벤트를 "작업 대기열"에만 삽입한다는 점에 유의해야 합니다. 메인 스레드는 지정된 콜백 함수를 실행하기 전에 현재 코드(실행 스택)의 실행이 완료될 때까지 기다려야 합니다. 현재 코드가 오래 걸리면 시간이 오래 걸릴 수 있으므로 setTimeout()에서 지정한 시간에 콜백 함수가 실행된다는 보장은 없습니다. 🎜🎜🎜🎜4. Node.js용 이벤트 루프🎜🎜🎜

이벤트 폴링은 주로 이벤트 대기열을 폴링합니다. 이벤트 생산자는 이벤트를 대기열에 넣고 대기열에 이벤트가 있는지 지속적으로 쿼리하는 이벤트 소비자라는 대기열의 다른 쪽 끝에 있습니다. 이벤트가 있으면 즉시 처리됩니다. 실행 중에 차단 작업이 현재 스레드의 읽기 대기열에 영향을 미치지 않도록 이벤트 소비자 스레드는 이러한 차단 작업을 구체적으로 수행하도록 스레드 풀에 맡깁니다.

Javascript 프런트엔드와 Node.js의 메커니즘은 이 이벤트 폴링 모델과 유사합니다. 어떤 사람들은 Node.js가 단일 스레드, 즉 이벤트 소비자가 단일 스레드이고 지속적으로 폴링된다고 생각합니다. 차단 작업이 있는 경우 어떻게 해야 합니까? 현재 단일 스레드 실행을 차단하지 않습니까?

실제로 Node.js 하단에도 스레드 풀이 있습니다. 스레드 풀은 다양한 차단 작업을 수행하는 데 특별히 사용됩니다. 이는 단일 스레드 메인 스레드의 이벤트 폴링 및 일부 작업 실행에 영향을 미치지 않습니다. 스레드 풀 작업이 완료된 후 이벤트 생성자 역할도 하며 작업 결과를 동일한 대기열에 넣습니다.

간단히 말하면, 이벤트 폴링 이벤트 루프에는 세 가지 구성 요소가 필요합니다.

이벤트 큐는 FIFO 모델에 속하며, 한쪽 끝은 이벤트 데이터를 푸시하고 다른 쪽 끝은 이벤트 데이터를 가져옵니다. , 이는 비동기식 느슨한 결합에 속합니다. 대기열의 읽기 폴링 스레드, 이벤트 소비자 및 이벤트 루프의 주인공입니다. 별도의 스레드 풀인 Thread Pool은 장시간 작업, 무거운 작업, 무거운 물리적 작업을 수행하는 데 특별히 사용됩니다.

Node.js도 단일 스레드 이벤트 루프이지만 작동 메커니즘은 브라우저 환경과 다릅니다.

위 그림에 따르면 Node.js의 동작 메커니즘은 다음과 같습니다.

V8 엔진은 JavaScript 스크립트를 구문 분석합니다. 구문 분석된 코드는 Node API를 호출합니다. libuv 라이브러리는 Node API의 실행을 담당합니다. 서로 다른 작업을 서로 다른 스레드에 할당하여 이벤트 루프(이벤트 루프)를 형성하고, 작업의 실행 결과를 비동기 방식으로 V8 엔진에 반환합니다. 그런 다음 V8 엔진은 결과를 사용자에게 반환합니다.

node.js의 핵심은 사실 libuv이 라이브러리임을 알 수 있습니다. 이 라이브러리는 C로 작성되었으며 멀티스레딩 기술을 사용할 수 있는 반면, Javascript 애플리케이션은 단일 스레드입니다.

Nodejs의 비동기 작업 실행 프로세스:

사용자가 작성한 코드는 단일 스레드이지만 nodejs는 내부적으로 단일 스레드가 아닙니다!

이벤트 메커니즘:

Node.js는 각 요청에 대한 작업을 수행하기 위해 여러 스레드를 사용하지 않고 모든 작업을 이벤트 큐에 추가한 다음 큐 이벤트를 반복하는 별도의 스레드를 갖습니다. 이벤트 루프 스레드는 이벤트 큐의 최상위 항목을 가져와 실행한 후 다음 항목을 가져옵니다. 오래 실행되거나 I/O를 차단하는 코드를 실행할 때

Node.js에는 HTTP 요청을 포함한 데이터베이스 파일 시스템과 같은 I/O 작업을 위해 이벤트 대기열을 지속적으로 폴링하는 단일 스레드만 있기 때문입니다. 차단 및 대기가 발생하기 쉬운 이러한 작업이 이 단일 스레드에서 구현되면 Javascript/Node.js는 실행을 기본 스레드 풀에 위임하고 스레드 풀에 콜백을 알립니다. 이러한 방식으로 단일 스레드는 계속해서 다른 작업을 수행합니다. 이러한 차단 작업이 완료되면 결과는 제공된 콜백 함수와 함께 대기열에 저장됩니다. 이러한 차단 작업의 결과는 콜백 함수의 입력 매개 변수로 사용되며 콜백 함수가 활성화되어 실행됩니다.

Node.js의 단일 스레드는 대기열 이벤트 읽기뿐만 아니라 콜백 기능 실행도 담당한다는 점에 유의하세요. 이는 다중 스레드 모드에서 단일 스레드인 것과 구별되는 주요 기능입니다. 큐 이벤트를 가져오는 역할만 담당하며 더 이상 다른 작업을 수행하지 않고 다른 스레드에 다른 작업을 맡깁니다. 특히 멀티 코어의 경우 하나의 CPU 코어가 큐 이벤트 읽기를 담당합니다. 코어는 활성화된 작업을 실행하는 역할을 담당합니다. 이 방법은 매우 비용이 많이 드는 CPU 컴퓨팅 작업에 가장 적합합니다. 결과적으로 Node..js의 실행 활성화 작업, 즉 콜백 함수의 작업은 여전히 ​​폴링을 담당하는 단일 스레드에서 실행되며, 이는 변환 등 CPU를 많이 사용하는 작업을 수행하는 것을 방지하기 위한 것입니다. JSON을 다른 데이터 형식으로 변환하는 등의 작업은 이벤트 폴링의 효율성에 영향을 미칩니다.

5. Nodejs 기능

NodeJS의 주요 기능: 비동기 메커니즘, 이벤트 중심.

이벤트 폴링의 모든 과정은 신규 사용자의 연결을 차단하지 않으며, 연결을 유지할 필요도 없습니다. 이러한 메커니즘을 기반으로 NodeJS는 이론적으로 사용자가 차례로 연결을 요청하면 응답할 수 있습니다. 따라서 NodeJS는 Java 및 PHP 프로그램보다 높은 동시성을 지원할 수 있습니다.

이벤트 큐를 유지하는 것도 비용이 필요하지만 NodeJS는 단일 스레드이므로 이벤트 큐가 길어질수록 응답을 받는 데 시간이 더 오래 걸리고 동시성 양은 여전히 ​​부족합니다.

RESTful API是NodeJS最理想的应用场景,可以处理数万条连接,本身没有太多的逻辑,只需要请求API,组织数据进行返回即可。

6. 实例

看一个具体实例:

console.log('1')
setTimeout(function() {
 console.log('2')
 new Promise(function(resolve) {
 console.log('4')
 resolve()
 }).then(function() {
 console.log('5')
 })
 setTimeout(() => {
 console.log('haha')
 })
 new Promise(function(resolve) {
 console.log('6')
 resolve()
 }).then(function() {
 console.log('66')
 })
})
setTimeout(function() {
 console.log('hehe')
}, 0)
new Promise(function(resolve) {
 console.log('7')
 resolve()
}).then(function() {
 console.log('8')
})
setTimeout(function() {
 console.log('9')
 new Promise(function(resolve) {
 console.log('11')
 resolve()
 }).then(function() {
 console.log('12')
 })
})
new Promise(function(resolve) {
 console.log('13')
 resolve()
}).then(function() {
 console.log('14')
})
// node1 : 1,7,13,8,14,2,4,6,hehe,9,11,5,66,12,haha // 结果不稳定
// node2 : 1,7,13,8,14,2,4,6,hehe,5,66,9,11,12,haha // 结果不稳定
// node3 : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha // 结果不稳定
// chrome : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha


chrome的运行比较稳定,而node环境下运行不稳定,可能会出现两种情况。

chrome运行的结果的原因是Promiseprocess.nextTick()的微任务Event Queue运行的权限比普通宏任务Event Queue权限高,如果取事件队列中的事件的时候有微任务,就先执行微任务队列里的任务,除非该任务在下一轮的Event Loop中,微任务队列清空了之后再执行宏任务队列里的任务。

相关推荐:

Node.js事件循环教程

javascript事件循环之强制梳理

深入理解Node.js 事件循环和回调函数

위 내용은 JS 및 Node.js의 이벤트 루프에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.