Как быстро начать работу с VUE3.0: начало обучения
Когда дело доходит до Node.js, я считаю, что большинство фронтенд-инженеров подумают о разработке серверов на его основе. Вам нужно только освоить JavaScript, чтобы стать полнофункциональным специалистом. инженер, но на самом деле смысл Node.js И это еще не все.
Для многих языков высокого уровня разрешения на выполнение могут достигать операционной системы, но JavaScript, работающий на стороне браузера, является исключением. Среда песочницы, созданная браузером, запирает разработчиков интерфейса в башне из слоновой кости в мире программирования. Однако появление Node.js компенсировало этот недостаток, и интерфейсные инженеры также могут достичь дна компьютерного мира.
Таким образом, значение Nodejs для интерфейсных инженеров заключается не только в предоставлении возможностей полнофункциональной разработки, но, что более важно, в открытии двери в базовый мир компьютеров для интерфейсных инженеров. Эта статья открывает эту дверь, анализируя принципы реализации Node.js.
В каталоге /deps хранилища исходного кода Node.js находится более дюжины зависимостей, включая модули, написанные на языке C (например, libuv, V8) и модули, написанные на языке JavaScript (например, acorn , плагины acorn). Как показано ниже.
Наиболее важными из них являются модули, соответствующие каталогам v8 и uv. Сам V8 не имеет возможности работать асинхронно, но он реализован с помощью других потоков в браузере. Именно поэтому мы часто говорим, что js является однопоточным, поскольку его механизм синтаксического анализа поддерживает только синхронный код. Но в Node.js асинхронная реализация в основном опирается на libuv. Давайте сосредоточимся на анализе принципа реализации libuv.
libuv — это библиотека асинхронного ввода-вывода, написанная на C и поддерживающая несколько платформ. В основном она решает проблему операций ввода-вывода, которые легко вызывают блокировку. Первоначально он был разработан специально для использования с Node.js, но с тех пор использовался другими модулями, такими как Luvit, Julia и pyuv. На рисунке ниже представлена структурная диаграмма libuv.
libuv имеет два асинхронных метода реализации, которые представляют собой две части, выделенные желтым полем слева и справа на рисунке выше.
Левая часть — это сетевой модуль ввода-вывода, который имеет разные механизмы реализации на разных платформах. В системах Linux для его реализации используется epoll, в OSX и других системах BSD используется KQueue, в системах SunOS используются порты событий, а в системах Windows — IOCP. Поскольку здесь используется базовый API операционной системы, его сложнее понять, поэтому я не буду его здесь представлять.
Правая часть включает модуль файлового ввода-вывода, модуль DNS и пользовательский код, реализующий асинхронные операции через пул потоков. Файловый ввод-вывод отличается от сетевого ввода-вывода. libuv не полагается на базовый API системы, но выполняет заблокированные операции ввода-вывода файлов в глобальном пуле потоков.
На следующем рисунке представлена диаграмма рабочего процесса опроса событий, представленная на официальном сайте libuv. Давайте проанализируем ее вместе с кодом.
Основной код цикла событий libuv реализован в функции uv_run(). Ниже приведена часть основного кода системы Unix. Хотя он написан на языке C, это язык высокого уровня, такой как JavaScript, поэтому его не так уж сложно понять. Самая большая разница может заключаться в звездочках и стрелках. Мы можем просто игнорировать звездочки. Например, цикл uv_loop_t* в параметре функции можно понимать как цикл переменных типа uv_loop_t. Стрелку «→» можно понимать как точку «.», например, цикл→stop_flag можно понимать как цикл.stop_flag.
int uv_run(цикл uv_loop_t*, режим uv_run_mode) { ... г = uv__loop_alive (цикл); если (!r) uv__update_time(цикл); while (r != 0 && цикл - >stop_flag == 0) { uv__update_time (цикл); uv__run_timers (цикл); ran_pending = uv__run_pending (цикл); uv__run_idle (цикл); uv__run_prepare(цикл);...uv__io_poll(цикл, тайм-аут); uv__run_check (цикл); uv__run_closing_handles(цикл);... }... }
uv__loop_alive
Эта функция используется для определения того, следует ли продолжать опрос событий. Если в объекте цикла нет активной задачи, она вернет 0 и выйдет из цикла.
В языке Си эта «задача» имеет профессиональное имя, то есть «дескриптор», под которым можно понимать переменную, указывающую на задачу. Дескрипторы можно разделить на две категории: запрос и дескриптор, которые представляют собой дескрипторы с коротким и длительным жизненным циклом соответственно. Конкретный код выглядит следующим образом:
static int uv__loop_alive(const uv_loop_t * цикл) { return uv__has_active_handles(цикл) || uv__has_active_reqs(цикл) || цикл - >closing_handles != NULL; }
uv__update_time
Чтобы уменьшить количество системных вызовов, связанных со временем, эта функция используется для кэширования текущего системного времени. Точность очень высока и может достигать уровня наносекунд, но единицей измерения по-прежнему являются миллисекунды.
Конкретный исходный код выглядит следующим образом:
UV_UNUSED(static void uv__update_time(uv_loop_t * цикл)) { цикл - > время = uv__hrtime(UV_CLOCK_FAST) / 1000000; }
uv__run_timers
выполняет функции обратного вызова, которые достигают порога времени в setTimeout() и setInterval(). Этот процесс выполнения реализуется посредством обхода цикла. Как видно из приведенного ниже кода, обратный вызов таймера сохраняется в данных структуры минимальной кучи. Он завершается, когда минимальная куча пуста или не достигла порога времени. .
Удалите таймер перед выполнением функции обратного вызова таймера. Если установлено повторение, его необходимо снова добавить в минимальную кучу, а затем выполнить обратный вызов таймера.
Конкретный код выглядит следующим образом:
void uv__run_timers(uv_loop_t * цикл) { структура heap_node * heap_node; uv_timer_t * дескриптор; для (;;) { heap_node = heap_min(timer_heap(loop)); если (heap_node == NULL) перерыв; дескриптор =Container_of(heap_node, uv_timer_t, heap_node); if (дескриптор -> таймаут > цикл -> время) перерыв; uv_timer_stop (дескриптор); uv_timer_again (дескриптор); дескриптор -> timer_cb (дескриптор); } }
uv__run_pending
обходит все функции обратного вызова ввода-вывода, хранящиеся в pending_queue, и возвращает 0, если pending_queue пуста, в противном случае возвращает 1 после выполнения функции обратного вызова в pending_queue;
Код выглядит следующим образом:
static int uv__run_pending(uv_loop_t * цикл) { ОЧЕРЕДЬ * q; ОЧЕРЕДЬ pq; uv__io_t * ш; if (QUEUE_EMPTY( & цикл - >pending_queue)) возвращает 0; QUEUE_MOVE( & цикл - >pending_queue, &pq); while (!QUEUE_EMPTY( & pq)) { q = QUEUE_HEAD( & pq); QUEUE_REMOVE (д); QUEUE_INIT(д); w = QUEUE_DATA(q, uv__io_t, pending_queue); ш -> cb(цикл, ш, POLLOUT); } возврат 1; }Все три функции
uvrun_idle / uvrun_prepare / uv__run_check
определяются посредством макрофункции UV_LOOP_WATCHER_DEFINE. Макрофункцию можно понимать как шаблон кода или функцию, используемую для определения функций. Макрос-функция вызывается три раза и передаются значения параметров name prepare, check иdleid соответственно. При этом определяются три функции: uvrun_idle, uvrun_prepare и uv__run_check.
Следовательно, их логика выполнения одинакова: все они проходят и извлекают объекты из очереди цикл->имя##_handles в соответствии с принципом «первым пришел — первым обслужен», а затем выполняют соответствующую функцию обратного вызова.
#define UV_LOOP_WATCHER_DEFINE(имя, тип) void uv__run_##name(uv_loop_t* цикл) { uv_##имя##_t* ч; ОЧЕРЕДЬ очередь; ОЧЕРЕДЬ* q; QUEUE_MOVE(&loop->name##_handles, &queue); while (!QUEUE_EMPTY(&очередь)) { q = QUEUE_HEAD(&очередь); h = QUEUE_DATA(q, uv_##имя##_t, очередь); QUEUE_REMOVE (д); QUEUE_INSERT_TAIL(&loop->name##_handles, q); ч->имя##_cb(h); } } UV_LOOP_WATCHER_DEFINE(подготовка, ПОДГОТОВКА) UV_LOOP_WATCHER_DEFINE(проверить, ПРОВЕРИТЬ) UV_LOOP_WATCHER_DEFINE(idle, IDLE)
uv__io_poll
uv__io_poll в основном используется для опроса операций ввода-вывода. Конкретная реализация будет варьироваться в зависимости от операционной системы. В качестве примера для анализа мы возьмем систему 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 (д); QUEUE_INIT(д); w = QUEUE_DATA(q, uv__io_t, watcher_queue); e.events = w ->pevents; e.data.fd = w ->fd; если (w -> события == 0) op = EPOLL_CTL_ADD; еще оп = EPOLL_CTL_MOD; if (epoll_ctl(loop - >backend_fd, op, w - >fd, &e)) { if (errno != EEXIST) abort(); if (epoll_ctl(loop - >backend_fd, EPOLL_CTL_MOD, w - >fd, &e)) abort(); } w ->events = w ->pevents; } для (;;) { для (я = 0; я <nfds; я++) { pe = события + я; fd = pe ->data.fd; w = цикл -> наблюдатели [fd]; pe - > события &= w - > POLLERR | if (pe - >events == POLLERR || pe - >events == POLLHUP) pe - >events |= w - >pevents & (POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI); if (pe ->events != 0) { if (w == &loop - >signal_io_watcher) have_signals = 1; иначе w -> cb (цикл, 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
обходит очередь, ожидающую закрытия, закрывает дескрипторы, такие как поток, tcp, udp и т. д., а затем вызывает close_cb, соответствующий дескриптору. Код выглядит следующим образом:
static void uv__run_closing_handles(uv_loop_t * цикл) { uv_handle_t * р; uv_handle_t * q; р = цикл -> закрывающие_обработчики; цикл -> Closing_handles = NULL; в то время как (р) { q = p -> next_closing; uv__finish_close(p); р = q; } }
Хотяprocess.nextTick и Promise являются асинхронными API, они не участвуют в опросе событий. У них есть свои собственные очереди задач, которые выполняются после завершения каждого шага опроса событий. Поэтому, когда мы используем эти два асинхронных API, нам нужно быть внимательными. Если во входящей функции обратного вызова выполняются длинные задачи или рекурсии, опрос событий будет заблокирован, что приведет к «голоданию» операций ввода-вывода.
Следующий код представляет собой пример, в котором функцию обратного вызова fs.readFile невозможно выполнить путем рекурсивного вызова prcoess.nextTick.
fs.readFile('config.json', (ошибка, данные) = >{... }) const traverse = () = >{ процесс.nextTick(переход) }
Чтобы решить эту проблему, вместо этого можно использовать setImmediate, поскольку setImmediate выполнит очередь функций обратного вызова в цикле событий. Очередь задачprocess.nextTick имеет более высокий приоритет, чем очередь задач Promise. По конкретным причинам обратитесь к следующему коду:
functionprocessTicksAndRejections() {. пусть так; делать { в то время как (tock = очередь.shift()) { const asyncId = tock[async_id_symbol]; emiteBefore(asyncId, tock[trigger_async_id_symbol], tock); пытаться { константный обратный вызов = tock.callback; если (tock.args === не определено) { перезвонить(); } еще { const args = tock.args; переключатель (args. length) { случай 1: обратный вызов (аргументы [0]); перерыв; случай 2: обратный вызов(args[0], args[1]); перерыв; случай 3: обратный вызов(args[0], args[1], args[2]); перерыв; случай 4: обратный вызов(args[0], args[1], args[2], args[3]); перерыв; по умолчанию: обратный вызов (...args); } } } окончательно { если (destroyHooksExist())emitDestroy(asyncId); } излучатьАфтер (асинхид); } запуститьМикрозадачи(); } while (! очередь. isEmpty () ||processPromiseRejections()); setHasTickScheduled (ложь); setHasRejectionToWarn (ложь); }
Как видно из функцииprocessTicksAndRejections(), функция обратного вызова очереди очереди сначала извлекается через цикл while, а функция обратного вызова в очередь очереди добавляется через процесс.nextTick. Когда цикл while завершается, вызывается функция runMicrotasks() для выполнения функции обратного вызова Promise.
что базовую структуру Node.js, основанную на libuv, можно разделить на две части. Одна часть — это сетевой ввод-вывод. Базовая реализация будет опираться на разные системные API в зависимости от операционных систем. Другая часть — это файл I. /O, DNS и код пользователя. Используйте пул потоков для обработки.
Основным механизмом libuv для обработки асинхронных операций является опрос событий. Общая операция заключается в обходе и выполнении функции обратного вызова в очереди.
Наконец, упоминается, что асинхронные API-процессы.nextTick и Promise не относятся к опросу событий. Неправильное использование приведет к блокировке опроса событий. Одним из решений является использование вместо этого setImmediate.