>  기사  >  웹 프론트엔드  >  JavaScript 이벤트 루프의 원리와 예에 대해 이야기해 보겠습니다.

JavaScript 이벤트 루프의 원리와 예에 대해 이야기해 보겠습니다.

WBOY
WBOY앞으로
2022-11-10 17:27:492066검색

이 기사는 이벤트 루프 관련 내용을 주로 소개하는 JavaScript에 대한 지식을 제공합니다. 모두에게 도움이 되기를 바랍니다.

JavaScript 이벤트 루프의 원리와 예에 대해 이야기해 보겠습니다.

【관련 권장사항: JavaScript 비디오 튜토리얼, web front-end

JavaScript 이벤트 루프를 이해하려면 매크로 작업과 마이크로 작업, JavaScript 단일 스레드 실행 프로세스 및 브라우저가 동반되는 경우가 많습니다. 비동기 메커니즘 및 기타 관련 문제, 브라우저와 NodeJS의 이벤트 루프 구현도 매우 다릅니다. 이벤트 루프에 익숙해지고 브라우저 작동 메커니즘을 이해하는 것은 JavaScript의 실행 프로세스를 이해하고 코드 실행 문제를 해결하는 데 큰 도움이 될 것입니다.

브라우저 JS의 비동기 실행 원칙

JS는 단일 스레드입니다. 즉, 동시에 한 가지 작업만 수행할 수 있으므로 생각해 보세요. 브라우저가 비동기 작업을 동시에 실행할 수 있는 이유는 무엇일까요?

브라우저가 다중 스레드이기 때문에 JS가 비동기 작업을 수행해야 할 때 브라우저는 작업을 수행하기 위해 다른 스레드를 시작합니다. 즉, "JS는 싱글 쓰레드(JS is Single-threaded)"라는 것은 JS 코드를 실행하는 쓰레드가 단 하나, 즉 브라우저가 제공하는 JS 엔진 쓰레드(메인 쓰레드)라는 뜻이다. 브라우저에는 타이머 스레드와 HTTP 요청 스레드도 있습니다. 이러한 스레드는 주로 JS 코드를 실행하는 데 사용되지 않습니다.

예를 들어, 메인 스레드에서 AJAX 요청을 보내야 하는 경우 이 작업은 실제로 요청을 보내기 위해 다른 브라우저 스레드(HTTP 요청 스레드)로 넘겨집니다. 콜백에서 실행되는 작업은 JS 엔진 스레드로 전달되어 실행됩니다. **즉, 요청을 보내는 작업을 실제로 수행하는 것은 브라우저이고, JS는 최종 콜백 처리만 담당합니다. **여기서 비동기식은 JS 자체에서 구현되지 않고 실제로 브라우저에서 제공하는 기능입니다.

Chrome을 예로 들어 보겠습니다. 브라우저에는 다중 스레드뿐만 아니라 렌더링 프로세스, GPU 프로세스, 플러그인 프로세스 등과 같은 다중 프로세스도 있습니다. 각 탭 페이지는 독립적인 렌더링 프로세스이므로 한 탭이 ​​비정상적으로 충돌하더라도 기본적으로 다른 탭은 영향을 받지 않습니다. 프런트 엔드 개발자로서 당신은 주로 렌더링 프로세스에 중점을 둡니다. 렌더링 프로세스에는 JS 엔진 스레드, HTTP 요청 스레드 및 타이머 스레드 등이 포함됩니다. 이러한 스레드는 JS가 브라우저에서 비동기 작업을 완료하는 기반을 제공합니다.

이벤트 중심 작업에 대한 간략한 분석

브라우저 비동기 작업의 실행 원리 뒤에는 실제로 일련의 이벤트 중심 메커니즘이 있습니다. 이벤트 트리거링, 작업 선택 및 작업 실행은 모두 이벤트 중심 메커니즘을 통해 수행됩니다. 간단히 말해서, 특정 작업은 특정 이벤트에 의해 트리거될 수 있습니다. 예를 들어, 클릭 이벤트는 프로그램에 의해 자동으로 트리거될 수도 있습니다. 브라우저의 타이머 스레드는 타이머가 끝난 후 타이머 이벤트를 트리거합니다. 이 기사의 주제는 이벤트 루프는 실제로 이벤트 중심 모델에서 이벤트를 관리하고 실행하는 프로세스 집합입니다.

게임 인터페이스에 이동 버튼과 캐릭터 모델이 있다고 가정해 보겠습니다. 오른쪽으로 이동하기 위해 클릭할 때마다 캐릭터 모델의 위치가 다시 렌더링되어 오른쪽으로 이동해야 합니다. 1픽셀씩. 렌더링 시점에 따라 다양한 방식으로 구현할 수 있습니다.

구현 방법 1: 이벤트 중심. 버튼을 클릭한 후 좌표 positionX가 수정되면 인터페이스 렌더링 이벤트가 즉시 발생하고 다시 렌더링이 발생합니다.

구현 방법 2: 상태 중심 또는 데이터 중심. 버튼을 클릭하면 좌표 위치X만 수정되고 인터페이스 렌더링은 실행되지 않습니다. 그 전에 타이머 setInterval이 시작되거나 requestAnimationFrame을 사용하여 positionX 변경 여부를 지속적으로 감지합니다. 변경사항이 있으면 즉시 다시 렌더링하세요.

브라우저의 클릭 이벤트 처리도 일반적으로 이벤트 중심입니다. 이벤트 기반 시스템에서는 이벤트가 트리거되면 트리거된 이벤트가 일시적으로 대기열에 순서대로 저장됩니다. JS 동기화 작업이 완료된 후 처리할 이벤트는 이 대기열에서 꺼내어 처리됩니다. 따라서 작업을 언제 가져올지, 어떤 작업을 먼저 가져올지는 이벤트 루프 프로세스에 의해 제어됩니다.

브라우저의 이벤트 루프

실행 스택 및 작업 대기열

JS가 코드 조각을 구문 분석할 때 동기화 코드를 순서대로, 즉 실행 스택에 정렬한 다음 내부의 함수를 순서대로 실행합니다. 비동기 작업이 발생하면 처리를 위해 다른 스레드로 넘겨집니다. 현재 실행 스택의 모든 동기화 코드가 실행된 후 완료된 비동기 작업의 콜백이 대기열에서 꺼내져 실행 스택에 추가됩니다. 실행을 계속하려면 비동기 작업이 다시 처리됩니다. 다른 비동기 작업이 완료된 후 콜백은 실행을 위해 실행 스택에서 꺼내어 작업 큐에 배치됩니다.

JS는 실행 스택의 메서드를 순차적으로 실행합니다. 메서드가 실행될 때마다 이 메서드에 대한 고유한 실행 환경(컨텍스트)이 생성됩니다. 이 메서드의 실행이 완료되면 현재 실행 환경이 삭제됩니다. 이 메서드가 팝업되고(즉, 소비가 완료됨) 다음 메서드로 계속됩니다.

이벤트 중심 모드에서는 작업 대기열에 새 작업이 있는지 감지하기 위해 하나 이상의 실행 루프가 포함되어 있음을 알 수 있습니다. 실행을 위해 비동기 콜백을 꺼내기 위해 계속해서 반복함으로써 이 프로세스는 이벤트 루프이고 각 루프는 이벤트 주기 또는 틱입니다.

매크로 작업과 마이크로 작업

작업 대기열은 두 개 이상 있습니다. 작업 유형에 따라 마이크로 작업 대기열과 매크로 작업 대기열로 나눌 수 있습니다.

이벤트 루프 과정에서 동기화 코드 실행이 완료된 후 실행 스택은 먼저 마이크로태스크 대기열에 실행해야 할 작업이 있는지 확인합니다. 그렇지 않으면 매크로태스크 대기열로 이동하여 확인합니다. 실행할 작업이 있는지 여부 등. 마이크로태스크는 일반적으로 현재 주기에서 먼저 실행되는 반면 매크로태스크는 다음 주기까지 대기합니다. 따라서 마이크로태스크는 일반적으로 매크로태스크보다 먼저 실행되며 마이크로태스크 대기열은 하나만 있지만 매크로태스크 대기열은 여러 개가 있을 수 있습니다. 또한 일반적인 클릭 및 키보드 이벤트도 매크로 작업에 속합니다. 일반적인 매크로 작업과 일반적인 마이크로 작업을 살펴보겠습니다.

공통 매크로 작업:

setTimeout()
  • setInterval()
  • setImmediate()
공통 마이크로 작업:

promise.then(), promise.catch()
  • 새로운 MutaionObserver()
  • process.nextTick()
  • console.log('同步代码1');setTimeout(() => {    console.log('setTimeout')
    }, 0)new Promise((resolve) => {  console.log('同步代码2')  resolve()
    }).then(() => {    console.log('promise.then')
    })console.log('同步代码3');// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"
  • 위 코드는 "동기화 코드 1", "동기화 코드 2", "동기화 코드 3", "promise.then", "의 순서로 출력됩니다. setTimeout "에 대한 구체적인 분석은 다음과 같습니다.

(1) setTimeout 콜백과 promise.then은 비동기적으로 실행되며 모든 동기 코드 후에 실행됩니다.

그런데 브라우저에서 setTimeout 지연이 0으로 설정되어 있으면 기본값은 4ms입니다. NodeJS는 1ms입니다. 정확한 값은 다를 수 있지만 0은 아닙니다.

(2) promise.then은 마지막에 작성되지만 마이크로 태스크이기 때문에 실행 순서는 setTimeout보다 우선합니다.

(3) new Promise는 동기식으로 실행되고 promise.then의 콜백은 비동기식입니다. 의.

위 코드의 실행 프로세스 데모를 살펴보겠습니다.

어떤 사람들은 이를 이렇게 이해하기도 합니다. 마이크로 작업은 현재 이벤트 루프의 끝에서 실행됩니다. 다음 이벤트 루프. 마이크로태스크와 매크로태스크의 본질적인 차이점을 살펴보겠습니다.

우리는 JS가 비동기 작업을 만나면 처리를 위해 해당 작업을 다른 스레드에 넘겨주고, 메인 스레드는 계속해서 동기 작업을 수행한다는 것을 알고 있습니다. 예를 들어, setTimeout의 타이밍은 브라우저의 타이머 스레드에 의해 처리됩니다. 타이밍이 끝나면 타이머 콜백 작업이 작업 대기열에 배치되고 기본 스레드가 이를 실행하기를 기다립니다. 앞서 언급했듯이 JS는 단일 스레드에서 실행되므로 비동기 작업을 수행하려면 다른 브라우저 스레드가 지원해야 합니다. 즉, 멀티 스레딩은 JS 비동기 작업의 확실한 기능입니다.

promise.then(마이크로태스크)의 처리를 분석해 보겠습니다. promise.then이 실행되면 V8 엔진은 비동기 작업을 다른 브라우저 스레드에 넘기지 않고 대신 현재 실행 스택의 실행이 완료된 후 콜백을 즉시 실행합니다. promise.then이 저장된 큐, promise.then 마이크로태스크는 멀티스레딩을 포함하지 않습니다. 일부 관점에서도 마이크로태스크는 작성 시 코드의 실행 순서를 변경할 뿐입니다.

setTimeout에는 타이머 스레드에 의해 실행되어야 하는 "타이밍 대기" 작업이 있으며, ajax 요청에는 HTTP 스레드에 의해 실행되어야 하는 "요청 보내기" 작업이 있는 반면 promise.then은 수행합니다. 다른 스레드에서 실행해야 하는 비동기 작업이 없으며 콜백만 있습니다. 있다고 하더라도 내부에 중첩된 또 다른 매크로 작업일 뿐입니다.

마이크로태스크와 매크로태스크의 본질적인 차이점에 대한 간략한 요약입니다.

  • 매크로 작업 기능: 실행해야 하는 명확한 비동기 작업이 있으며 다른 비동기 스레드 지원이 필요합니다.
  • 마이크로 작업 기능: 실행할 명확한 비동기 작업은 없으며 콜백만 필요합니다. 다른 비동기 스레드 지원은 필요하지 않습니다.

타이머 오류

이벤트 루프에서는 항상 동기 코드가 먼저 실행된 다음 실행을 위해 작업 대기열에서 비동기 콜백을 가져옵니다. setTimeout이 실행되면 브라우저는 호출 시간을 측정하기 위해 새 스레드를 시작하고 타이머 이벤트가 트리거되고 콜백이 매크로 작업 대기열에 저장되어 JS 기본 스레드가 실행될 때까지 기다립니다. 이때 메인 스레드가 아직 동기화 작업을 실행하고 있다면 이 시점의 매크로 작업을 먼저 일시 중단해야 하며 이로 인해 타이머가 정확하지 않게 되는 문제가 발생합니다. 동기화 코드가 길어질수록 타이머의 오류가 커집니다. 동기화 코드뿐만 아니라 마이크로태스크가 먼저 실행되기 때문에 마이크로태스크도 타이밍에 영향을 미칩니다. 동기화 코드에 무한 루프가 있거나 마이크로태스크의 재귀가 지속적으로 다른 마이크로태스크를 시작하는 경우 매크로태스크의 코드가 타이밍에 영향을 미칩니다. 결코 구현되지 않을 수 있습니다. 따라서 메인 스레드 코드의 실행 효율성을 높이는 것이 매우 중요합니다.

아주 간단한 시나리오는 인터페이스에 초 단위까지 정확하고 매초 시간을 업데이트하는 시계가 있다는 것입니다. 때때로 초가 2초 간격을 건너뛰는 경우가 있다는 것을 알 수 있습니다. 이것이 바로 그 이유입니다.

뷰 업데이트 렌더링

마이크로태스크 큐 실행이 완료된 후, 즉 이벤트 루프가 종료된 후 브라우저는 뷰 렌더링을 수행합니다. 물론 여기서 브라우저 최적화가 이루어지며 여러 루프의 결과가 나올 수 있습니다. 뷰 새로 고침을 수행하기 위해 병합되므로 이벤트 루프 후에 뷰가 업데이트되므로 Dom의 모든 작업이 반드시 뷰를 즉시 새로 고치지는 않습니다. requestAnimationFrame 콜백은 뷰가 다시 그려지기 전에 실행되므로 requestAnimationFrame이 마이크로태스크인지 매크로태스크인지 논란의 여지가 있습니다. 이러한 관점에서는 마이크로태스크도 아니고 매크로태스크도 아니어야 합니다.

NodeJS의 이벤트 루프

JS 엔진 자체는 이벤트 루프 메커니즘을 구현하지 않습니다. 이는 호스트에 의해 구현되며, NodeJS에도 자체 이벤트 루프가 있습니다. . NodeJS에는 루프 + 작업 대기열 프로세스도 있으며 마이크로 작업이 매크로 작업보다 우선순위가 높습니다. 그러나 브라우저와도 일부 차이점이 있으며 몇 가지 새로운 작업 유형 및 작업 단계가 추가되었습니다. 다음으로 NodeJS의 이벤트 루프 프로세스를 소개합니다.

NodeJS의 비동기식 메서드

모두 V8 엔진을 기반으로 하기 때문에 브라우저에 포함된 비동기식 메서드도 NodeJS에서도 동일합니다. NodeJS에는 다른 일반적인 비동기 형식도 있습니다.

  • 파일 I/O: 로컬 파일을 비동기적으로 로드합니다.
  • setImmediate(): setTimeout을 0ms로 설정하는 것과 유사하며 특정 동기화 작업이 완료된 후 즉시 실행됩니다.
  • process.nextTick(): 특정 동기화 작업이 완료된 후 즉시 실행됩니다.
  • server.close, 소켓.on('close',...) 등: 콜백을 닫습니다.

위의 형태가 setTimeout, promise 등이 동시에 존재한다면 코드의 실행 순서를 어떻게 분석할지 상상해 보세요. NodeJS의 이벤트 루프 메커니즘을 이해하면 명확해질 것입니다.

이벤트 루프 모델

NodeJS의 크로스 플랫폼 기능과 이벤트 루프 메커니즘은 모두 Libuv 라이브러리를 기반으로 구현됩니다. 이 라이브러리의 특정 내용에 신경 쓸 필요가 없습니다. Libuv 라이브러리가 이벤트 기반이며 다양한 플랫폼에서 API 구현을 캡슐화하고 통합한다는 것만 알면 됩니다.

NodeJS에서 V8 엔진은 JS 코드를 구문 분석하고 Node API를 호출합니다. 그런 다음 Node API는 할당을 위해 작업을 Libuv에 전달하고 마지막으로 실행 결과를 V8 엔진에 반환합니다. 이러한 작업의 실행을 관리하기 위해 일련의 이벤트 루프 프로세스가 Libux에서 구현되므로 NodeJS의 이벤트 루프는 주로 Libuv에서 완료됩니다.

Libuv에서 루프가 어떤 모습인지 살펴보겠습니다.

이벤트 루프의 각 단계

NodeJS에서 JS 실행 시 주로 신경써야 할 프로세스는 다음과 같은 단계로 나누어집니다. 아래 각 단계에는 해당 단계가 실행될 때 별도의 작업 대기열이 있습니다. 현재 스테이지의 작업 대기열에 처리해야 할 작업이 있는지 여부입니다.

  • timers 阶段:执行所有 setTimeout() 和 setInterval() 的回调。
  • pending callbacks 阶段:某些系统操作的回调,如  TCP  链接错误。除了 timers、close、setImmediate 的其他大部分回调在此阶段执行。
  • poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等。V8 引擎将 JS 代码解析并传入 Libuv 引擎后首先进入此阶段。如果此阶段任务队列已经执行完了,则进入 check 阶段执行 setImmediate 回调(如果有 setImmediate),或等待新的任务进来(如果没有 setImmediate)。在等待新的任务时,如果有 timers 计时到期,则会直接进入 timers 阶段。此阶段可能会阻塞等待。
  • check 阶段:setImmediate 回调函数执行。
  • close callbacks 阶段:关闭回调执行,如 socket.on('close', ...)。

上面每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段。这里也是与浏览器中逻辑差异较大的地方,不过浏览器不用区分这些阶段,也少了很多异步操作类型,所以不用刻意去区分两者区别。代码如下所示:

const fs = require('fs');
fs.readFile(__filename, (data) => {    // poll(I/O 回调) 阶段
    console.log('readFile')    Promise.resolve().then(() => {        console.error('promise1')
    })    Promise.resolve().then(() => {        console.error('promise2')
    })
});setTimeout(() => {    // timers 阶段
    console.log('timeout');    Promise.resolve().then(() => {        console.error('promise3')
    })    Promise.resolve().then(() => {        console.error('promise4')
    })
}, 0);// 下面代码只是为了同步阻塞1秒钟,确保上面的异步任务已经准备好了var startTime = new Date().getTime();var endTime = startTime;while(endTime - startTime < 1000) {
    endTime = new Date().getTime();
}// 最终输出 timeout promise3 promise4 readFile promise1 promise2

另一个与浏览器的差异还体现在同一个阶段里的不同任务执行,在 timers 阶段里面的宏任务、微任务测试代码如下所示:

setTimeout(() => {  console.log('timeout1')    Promise.resolve().then(function() {    console.log('promise1')
  })
}, 0);setTimeout(() => {  console.log('timeout2')    Promise.resolve().then(function() {    console.log('promise2')
  })
}, 0);
  • 浏览器中运行

    每次宏任务完成后都会优先处理微任务,输出“timeout1”、“promise1”、“timeout2”、“promise2”。

  • NodeJS 中运行

    因为输出 timeout1 时,当前正处于  timers 阶段,所以会先将所有 timer 回调执行完之后再执行微任务队列,即输出“timeout1”、“timeout2”、“promise1”、“promise2”。

上面的差异可以用浏览器和 NodeJS 10 对比验证。是不是感觉有点反程序员?因此 NodeJS 在版本 11 之后,就修改了此处逻辑使其与浏览器尽量一致,也就是每个 timer 执行后都先去检查一下微任务队列,所以 NodeJS 11 之后的输出已经和浏览器一致了。

nextTick、setImmediate 和 setTimeout

实际项目中我们常用 Promise 或者 setTimeout 来做一些需要延时的任务,比如一些耗时计算或者日志上传等,目的是不希望它的执行占用主线程的时间或者需要依赖整个同步代码执行完成后的结果。

NodeJS 中的 process.nextTick() 和 setImmediate() 也有类似效果。其中 setImmediate() 我们前面已经讲了是在 check 阶段执行的,而 process.nextTick() 的执行时机不太一样,它比 promise.then() 的执行还早,在同步任务之后,其他所有异步任务之前,会优先执行 nextTick。可以想象是把 nextTick 的任务放到了当前循环的后面,与 promise.then() 类似,但比 promise.then() 更前面。意思就是在当前同步代码执行完成后,不管其他异步任务,先尽快执行 nextTick。如下面的代码,因此这里的 nextTick 其实应该更符合“setImmediate”这个命名才对。

setTimeout(() => {    console.log('timeout');
}, 0);Promise.resolve().then(() => {    console.error('promise')
})
process.nextTick(() => {    console.error('nextTick')
})// 输出:nextTick、promise、timeout

接下来我们再来看看 setImmediate 和 setTimeout,它们是属于不同的执行阶段了,分别是 timers 阶段和 check 阶段。

setTimeout(() => {  console.log('timeout');
}, 0);setImmediate(() => {  console.log('setImmediate');
});// 输出:timeout、 setImmediate

分析上面代码,第一轮循环后,分别将 setTimeout   和 setImmediate 加入了各自阶段的任务队列。第二轮循环首先进入  timers 阶段,执行定时器队列回调,然后  pending callbacks 和 poll 阶段没有任务,因此进入check 阶段执行 setImmediate 回调。所以最后输出为“timeout”、“setImmediate”。当然这里还有种理论上的极端情况,就是第一轮循环结束后耗时很短,导致 setTimeout 的计时还没结束,此时第二轮循环则会先执行 setImmediate 回调。

再看这下面一段代码,它只是把上一段代码放在了一个 I/O 任务回调中,它的输出将与上一段代码相反。

const fs = require('fs');
fs.readFile(__filename, (data) => {    console.log('readFile');    setTimeout(() => {        console.log('timeout');
    }, 0);    setImmediate(() => {        console.log('setImmediate');
    });
});// 输出:readFile、setImmediate、timeout

如上面代码所示:

  • 첫 번째 루프 라운드에서 실행해야 하는 비동기 작업 대기열이 없습니다.
  • 두 번째 루프 라운드의 타이머 및 기타 단계에는 작업이 없으며 폴링 단계에만 I/O 콜백 작업이 있습니다.
  • 이전 이벤트 단계를 참조하세요. 다음으로 폴 단계에서는 setImmediate 작업 대기열이 있는지 감지하고 확인 단계로 들어갑니다. 그렇지 않으면 타이머 작업 콜백이 있는지 판단됩니다. , 타이머 단계로 돌아갑니다. 따라서 setImmediate를 실행하고 "setImmediate"를 출력하려면 확인 단계로 들어가야 합니다.
  • 그런 다음 마지막 콜백 종료 단계에 들어가고 이 주기가 종료됩니다.
  • 루프를 실행하고 타이머 단계로 들어가서 "timeout"을 출력합니다.

따라서 "setImmediate"의 최종 출력은 "timeout" 이전입니다. 두 가지의 실행 순서는 현재 실행 단계와 관련이 있음을 알 수 있다.

【관련 추천: JavaScript 비디오 튜토리얼, 웹 프론트엔드

위 내용은 JavaScript 이벤트 루프의 원리와 예에 대해 이야기해 보겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.im에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제