Node.js визуализация: promise, async/await and process.nextTick
В статье предполагается, что вы знакомы с event loop и его фазами. Promise спасли положение тысячам разработчиков JavaScript, но знаем ли мы, как они работают под капотом?
В свое время ад обратного вызова был нормой для обработки асинхронных операций. Простой JavaScript-скрипт для чтения файла и вставки его содержимого в базу данных мог бы быть похож на этот фрагмент.
Нам больше не нужно иметь дело с этой вложенной структурой из-за promise и async/await.
Демистификация promise
Определение MDN для promise - это «возможное завершение (или сбой) асинхронной операции».
Определение выглядит так, как будто мы имеем дело с какой-то формой непонятной магии, но, проще говоря, promise - это контейнер для будущей ценности.
promise ведет себя в определенной степени как браузер, содержащий либо желаемое содержимое страницы, либо сообщение об ошибке. Всякий раз, когда мы переходим по адресу, ваш веб-браузер может находиться в трех разных состояниях:
- При извлечении данных может быть показана пустая страница или загрузочный счетчик.
- Содержимое страницы успешно отображено.
- Может появиться ошибка.
Promise может иметь три статуса:
pending, когда, например, запрос не завершен;fulfilled, когда сетевой запрос будет завершен;rejected, когда асинхронный запрос завершится.
Практический пример статуса promise
Недавно созданное promise находится в состоянии pending.
Аналогично, операция ввода-вывода (I/O), такая как запрос к базе данных, находится в pending до тех пор, пока она не разрешится или не выдаст ошибку.
Мы можем прикрепить прослушиватель .then/.catch для обработки будущего значения promise. Как только вызывается функция resolve/reject, promise меняет свой статус с pending на fulfilled/rejected.
async/await
Современный JavaScript полностью основан на async/await, но эти два выражения являются альтернативным способом написания promise.
Мы можем переписать фрагмент базы данных с promise на async/await или наоборот.
Мы можем перепутать синтаксис и сбить с толку наших коллег (крайне не рекомендуется для удобства чтения и согласованности).
Promise под капотом
Promise, по-видимому, обрабатываются синхронно, но их выполнение откладывается до определенного момента в будущем.
Следующий сценарий показывает эту console.log запускаются перед обратным вызовом promise.
Под капотом двигатель V8 запоминает функцию обратного вызова и помещает ее в очередь микрозадач, когда promise resolve/reject.
Как только стек вызовов опустеет, V8 сможет обрабатывать микрозадачи.
Очередь микрозадач Promise и цикл событий
Очередь микрозадач и цикл событий ожидают, пока стек вызовов опустеет, прежде чем отправлять задачи на выполнение, но какая из них имеет более высокий приоритет?
Следующий сценарий показывает, что Node.js и его внутренние компоненты всегда будут пытаться исчерпать очередь микрозадач, прежде чем передавать управление обратно циклу событий и его фазам:
setTimeout и Promise.reject ставят в очередь макрозадачу на этапе таймера и микрозадачу.
Обратный вызов в очереди микрозадач выполняется перед фазой таймера цикла событий. Как только микрозадач не останется, цикл событий может возобновить свое выполнение.
Обратный вызов setTimeout вызывает setImmediate и запускает другую микрозадачу promise, которая будет выполнена в пустом стеке вызовов.
process.nextTick
process.nextTick также планирует микрозадачи, которые Node.js управляется в выделенной очереди и имеет более высокий приоритет (в CommonJS), чем очередь promise и фазы цикла событий.
В следующем сценарии, Node.js помещает обратный вызов в их очередь.
Как только стек вызовов опустеет, Node.js обрабатывает микрозадачи process.nextTick, за которыми следуют очередь promise и фазы цикла событий.