VUE3.0을 빠르게 시작하는 방법: 학습 시작하기
Node.js에 관해서라면 대부분의 프런트 엔드 엔지니어는 이를 기반으로 서버를 개발할 것이라고 생각합니다. 풀 스택이 되려면 JavaScript만 마스터하면 됩니다. 엔지니어이지만 사실 Node.js의 의미는 그게 전부가 아닙니다.
많은 고급 언어의 경우 실행 권한이 운영 체제에 도달할 수 있지만 브라우저 측에서 실행되는 JavaScript는 예외입니다. 브라우저가 만든 샌드박스 환경은 프로그래밍 세계의 상아탑에 프런트 엔드 엔지니어를 봉인합니다. 하지만 Node.js의 등장으로 이러한 단점이 보완됐고, 프론트엔드 엔지니어도 컴퓨터 세계의 밑바닥까지 갈 수 있게 됐다.
따라서 프론트 엔드 엔지니어에게 Nodejs의 중요성은 풀 스택 개발 기능을 제공하는 것뿐만 아니라 더 중요한 것은 프론트 엔드 엔지니어에게 기본 컴퓨터 세계로의 문을 여는 것입니다. 이 기사는 Node.js의 구현 원칙을 분석하여 그 문을 엽니다.
Node.js 소스 코드 웨어하우스의 /deps 디렉토리에는 C 언어로 작성된 모듈(예: libuv, V8)과 JavaScript 언어로 작성된 모듈(예: acorn)을 포함하여 12개 이상의 종속성이 있습니다. , acorn-plugins)을 참조하세요.
그 중 가장 중요한 것은 v8 및 uv 디렉토리에 해당하는 모듈입니다. V8 자체에는 비동기식으로 실행되는 기능이 없지만 브라우저의 다른 스레드의 도움으로 구현됩니다. 파싱 엔진이 동기식 파싱 코드만 지원하기 때문에 js가 단일 스레드라고 말하는 경우가 많습니다. 하지만 Node.js에서는 비동기 구현이 주로 libuv에 의존합니다. libuv의 구현 원리를 집중적으로 살펴보겠습니다.
libuv는 다중 플랫폼을 지원하는 C로 작성된 비동기 I/O 라이브러리로, 주로 차단을 쉽게 일으키는 I/O 작업 문제를 해결합니다. 원래 Node.js와 함께 사용하기 위해 특별히 개발되었지만 이후 Luvit, Julia 및 pyuv와 같은 다른 모듈에서 사용되었습니다. 아래 그림은 libuv의 구조도이다.
libuv에는 두 가지 비동기 구현 방법이 있는데, 위 그림의 왼쪽과 오른쪽에 있는 노란색 상자로 선택된 두 부분입니다.
왼쪽 부분은 네트워크 I/O 모듈로, 서로 다른 플랫폼에서 서로 다른 구현 메커니즘을 가지고 있습니다. Linux 시스템은 epoll을 사용하여 구현하고, OSX 및 기타 BSD 시스템은 KQueue를 사용하고, SunOS 시스템은 이벤트 포트를 사용하고, Windows 시스템은 IOCP를 사용합니다. 운영 체제의 기본 API와 관련되어 있기 때문에 이해하기가 더 복잡하므로 여기서는 소개하지 않겠습니다.
오른쪽 부분에는 스레드 풀을 통해 비동기 작업을 구현하는 파일 I/O 모듈, DNS 모듈 및 사용자 코드가 포함됩니다. 파일 I/O는 네트워크 I/O와 다릅니다. libuv는 시스템의 기본 API에 의존하지 않지만 전역 스레드 풀에서 차단된 파일 I/O 작업을 수행합니다.
다음 그림은 libuv 공식 웹사이트에서 제공하는 이벤트 폴링 워크플로 다이어그램을 코드와 함께 분석해 보겠습니다.
libuv 이벤트 루프의 핵심 코드는 uv_run() 함수에 구현되어 있습니다. 다음은 Unix 시스템의 핵심 코드 중 일부입니다. C언어로 작성되었지만 자바스크립트와 같은 고급 언어이기 때문에 이해하는데 그리 어렵지는 않습니다. 가장 큰 차이점은 별표와 화살표일 수 있습니다. 별표는 무시해도 됩니다. 예를 들어, 함수 매개변수의 uv_loop_t* 루프는 uv_loop_t 유형의 변수 루프로 이해될 수 있습니다. 화살표 "→"는 마침표 "."로 이해될 수 있습니다. 예를 들어 loop→stop_flag는 loop.stop_flag로 이해될 수 있습니다.
int uv_run(uv_loop_t* 루프, uv_run_mode 모드) { ... r = uv__loop_alive(루프); if (!r) uv__update_time(loop); while (r != 0 && 루프 - >stop_flag == 0) { uv__update_time(loop); uv__run_timers(루프); ran_pending = uv__run_pending(루프); uv__run_idle(루프); uv__run_prepare(loop);...uv__io_poll(loop, timeout); uv__run_check(루프); uv__run_closing_handles(루프);... }... }
uv__loop_alive
이 함수는 이벤트 폴링을 계속할지 여부를 결정하는 데 사용됩니다. 루프 개체에 활성 작업이 없으면 0을 반환하고 루프를 종료합니다.
C 언어에서 이 "작업"은 전문적인 이름, 즉 "핸들"을 가지며, 이는 작업을 가리키는 변수로 이해될 수 있습니다. 핸들은 요청과 핸들이라는 두 가지 범주로 나눌 수 있으며 각각 짧은 수명 주기 핸들과 긴 수명 주기 핸들을 나타냅니다. 구체적인 코드는 다음과 같습니다:
static int uv__loop_alive(const uv_loop_t * loop) { return uv__has_active_handles(loop) || uv__has_active_reqs(loop) || 루프 - >closing_handles != NULL; }
uv__update_time
시간과 관련된 시스템 호출 수를 줄이기 위해 이 함수를 사용하여 현재 시스템 시간을 캐시합니다. 정확도는 매우 높아 나노초 수준에 도달할 수 있지만 단위는 여전히 밀리초입니다.
구체적인 소스코드는 다음과 같습니다.
UV_UNUSED(static void uv__update_time(uv_loop_t * loop)) { 루프 - >time = uv__hrtime(UV_CLOCK_FAST) / 1000000; }
uv__run_timers는
setTimeout() 및 setInterval()에서 시간 임계값에 도달하는 콜백 함수를 실행합니다. 이 실행 프로세스는 아래 코드에서 볼 수 있듯이 최소 힙 구조의 데이터에 저장되며 최소 힙이 비어 있거나 시간 임계값에 도달하지 않은 경우 종료됩니다. .
타이머 콜백 기능을 실행하기 전에 타이머를 제거한 후 반복이 설정된 경우 최소 힙에 다시 추가한 후 타이머 콜백이 실행됩니다.
구체적인 코드는 다음과 같습니다:
void uv__run_timers(uv_loop_t * loop) { 구조체 heap_node * heap_node; uv_timer_t * 핸들; 을 위한 (;;) { heap_node = heap_min(timer_heap(loop)); if (heap_node == NULL) 중단; 핸들 = 컨테이너_of(heap_node, uv_timer_t, heap_node); if (handle - >timeout > loop - >time) break; uv_timer_stop(핸들); uv_timer_again(핸들); 핸들 ->timer_cb(핸들); } }
uv__run_pending은
보류 중인_queue에 저장된 모든 I/O 콜백 함수를 순회하고, 보류 중인_큐가 비어 있으면 0을 반환하고, 그렇지 않으면 보류 중인_큐에서 콜백 함수를 실행한 후 1을 반환합니다.
코드는 다음과 같습니다:
static int uv__run_pending(uv_loop_t * loop) { 대기열 * q; 대기열 pq; uv__io_t * w; if (QUEUE_EMPTY( & loop - >pending_queue)) 0을 반환합니다. QUEUE_MOVE( & 루프 - >pending_queue, &pq); 동안 (!QUEUE_EMPTY( & pq)) { q = QUEUE_HEAD( & pq); QUEUE_REMOVE(q); QUEUE_INIT(q); w = QUEUE_DATA(q, uv__io_t, 보류 중인_큐); w - >cb(루프, w, POLLOUT); } 1을 반환합니다. }
uvrun_idle / uvrun_prepare / uv__run_check
세 가지 함수는모두 매크로 함수 UV_LOOP_WATCHER_DEFINE을 통해 정의됩니다. 매크로 함수는 코드 템플릿 또는 함수를 정의하는 데 사용되는 함수로 이해될 수 있습니다. 매크로 함수가 3번 호출되고 이름 매개변수 값인 prepare, check, idle이 각각 전달됩니다. 동시에 uvrun_idle, uvrun_prepare, uv__run_check 세 가지 함수가 정의됩니다.
따라서 실행 논리는 모두 일관됩니다. 선입 선출 원칙에 따라 대기열 loop->name##_handles의 개체를 루프를 통해 꺼낸 다음 해당 콜백 함수를 실행합니다.
#define UV_LOOP_WATCHER_DEFINE(이름, 유형) void uv__run_##name(uv_loop_t* 루프) { uv_##이름##_t* h; QUEUE 대기열; 대기열* q; QUEUE_MOVE(&loop->name##_handles, &queue); while (!QUEUE_EMPTY(&큐)) { q = QUEUE_HEAD(&큐); h = QUEUE_DATA(q, uv_##name##_t, 대기열); QUEUE_REMOVE(q); QUEUE_INSERT_TAIL(&loop->name##_handles, q); h->이름##_cb(h); } } UV_LOOP_WATCHER_DEFINE(준비, 준비) UV_LOOP_WATCHER_DEFINE(확인, 확인) UV_LOOP_WATCHER_DEFINE(idle, IDLE)
uv__io_poll
uv__io_poll은 주로 I/O 작업을 폴링하는 데 사용됩니다. 구체적인 구현은 운영 체제에 따라 달라집니다. 분석을 위해 Linux 시스템을 예로 들어보겠습니다.
uv__io_poll 함수의 핵심은 두 개의 루프 코드입니다. 코드의 일부는 다음과 같습니다.
void uv__io_poll(uv_loop_t * loop, int timeout) while (!QUEUE_EMPTY( & 루프 - >watcher_queue)) { q = QUEUE_HEAD( & 루프 - >watcher_queue); QUEUE_REMOVE(q); QUEUE_INIT(q); w = QUEUE_DATA(q, uv__io_t, watcher_queue); e.events = w ->페벤트; e.data.fd = w ->fd; if (w - >events == 0) op = EPOLL_CTL_ADD; 그렇지 않으면 op = EPOLL_CTL_MOD; if (epoll_ctl(loop - >backend_fd, op, w - >fd, &e)) { if (errno != EEXIST) 중단(); if (epoll_ctl(loop - >backend_fd, EPOLL_CTL_MOD, w - >fd, &e)) abort(); } w - >이벤트 = w - >페벤트; } 을 위한 (;;) { for (i = 0; i < nfds; i++) { pe = 이벤트 + i; fd = pe ->data.fd; w = 루프 - >감시자[fd]; pe - >이벤트 &= w - >pevents | if (pe - >events == POLLERR || pe - >events == POLLHUP) pe - >events |= w - >pevents & (POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI); if (pe - >이벤트 != 0) { if (w == &loop - >signal_io_watcher) have_signals = 1; else w - >cb(loop, w, pe - >이벤트); 네벤츠++; } } if (have_signals != 0) 루프 - >signal_io_watcher.cb(loop, &loop - >signal_io_watcher, POLLIN); }... }
while 루프에서 관찰자 큐 watcher_queue를 순회하고 이벤트 및 파일 설명자를 꺼내 이벤트 객체 e에 할당한 다음 epoll_ctl 함수를 호출하여 epoll 이벤트를 등록하거나 수정합니다.
for 루프에서는 epoll에서 대기 중인 파일 설명자를 꺼내어 nfds에 할당한 다음 nfds를 순회하여 콜백 함수를 실행합니다.
uv__run_closing_handles는
닫히기를 기다리는 대기열을 순회하고 stream, tcp, udp 등의 핸들을 닫은 다음 해당 핸들에 해당하는 close_cb를 호출합니다. 코드는 다음과 같습니다:
static void uv__run_closing_handles(uv_loop_t * loop) { uv_handle_t *p; uv_handle_t * q; p = 루프 ->closing_handles; 루프 ->closing_handles = NULL; 동안 (p) { q = p ->next_closing; uv__finish_close(p); p = q; } }
process.nextTick 및 Promise는 모두 비동기 API이지만 이벤트 폴링의 일부는 아닙니다. 이벤트 폴링의 각 단계가 완료된 후 실행되는 자체 작업 대기열이 있습니다. 따라서 이 두 가지 비동기 API를 사용할 때는 주의가 필요합니다. 들어오는 콜백 함수에서 긴 작업이나 재귀가 수행되면 이벤트 폴링이 차단되어 I/O 작업이 "고갈"됩니다.
다음 코드는 prcoess.nextTick을 재귀적으로 호출하여 fs.readFile의 콜백 함수를 실행할 수 없는 예입니다.
fs.readFile('config.json', (err, data) = >{... }) const 트래버스 = () = >{ process.nextTick(트래버스) }
이 문제를 해결하려면 setImmediate를 대신 사용할 수 있습니다. 왜냐하면 setImmediate가 이벤트 루프에서 콜백 함수 대기열을 실행하기 때문입니다. process.nextTick 작업 대기열은 Promise 작업 대기열보다 우선순위가 높습니다. 구체적인 이유는 다음 코드를 참조하세요.
function processTicksAndRejections() { 톡하자; 하다 { while (tock = queue.shift()) { const asyncId = tock[async_id_symbol]; EmitBefore(asyncId, tock[trigger_async_id_symbol], tock); 노력하다 { const 콜백 = tock.callback; if (tock.args === 정의되지 않음) { 콜백(); } 또 다른 { const args = tock.args; 스위치(인수 길이) { 사례 1: 콜백(args[0]); 부서지다; 사례 2: 콜백(args[0], args[1]); 부서지다; 사례 3: 콜백(args[0], args[1], args[2]); 부서지다; 사례 4: 콜백(args[0], args[1], args[2], args[3]); 부서지다; 기본: 콜백(...args); } } } 마지막으로 { if (destroyHooksExist()) EmitDestroy(asyncId); } EmitAfter(asyncId); } runMicrotasks(); } while (! queue . isEmpty () || processPromiseRejections()); setHasTickScheduled(false); setHasRejectionToWarn(false); }
processTicksAndRejections() 함수에서 볼 수 있듯이, while 루프를 통해 먼저 큐 큐의 콜백 함수를 꺼내고, process.nextTick을 통해 큐 큐에 있는 콜백 함수를 추가한다. while 루프가 끝나면 runMicrotasks() 함수가 호출되어 Promise 콜백 함수를 실행합니다.
libuv에 의존하는 Node.js의 핵심 구조는 두 부분으로 나눌 수 있습니다. 한 부분은 네트워크 I/O이며, 다른 부분은 다른 운영 체제에 따라 다른 시스템 API에 의존합니다. /O, DNS, 사용자 코드입니다. 처리를 위해 스레드 풀을 사용합니다.
비동기 작업을 처리하는 libuv의 핵심 메커니즘은 이벤트 폴링입니다. 이벤트 폴링은 여러 단계로 나누어져 있습니다. 일반적인 작업은 대기열에서 콜백 함수를 탐색하고 실행하는 것입니다.
마지막으로 비동기 API process.nextTick 및 Promise는 이벤트 폴링에 속하지 않는다고 언급되어 있습니다. 부적절하게 사용하면 이벤트 폴링이 차단됩니다. 대신 setImmediate를 사용하는 것입니다.