Node.js визуализация: promise, async/await and process.nextTick
В статье предполагается, что вы знакомы с event loop и его фазами. Promise спасли положение тысячам разработчиков JavaScript, но знаем ли мы, как они работают под капотом?
В свое время ад обратного вызова был нормой для обработки асинхронных операций. Простой JavaScript-скрипт для чтения файла и вставки его содержимого в базу данных мог бы быть похож на этот фрагмент.
![](/static/storage/87761180936671227552332187041862069030.png)
Нам больше не нужно иметь дело с этой вложенной структурой из-за promise и async
/await
.
![](/static/storage/222169000238905359515397929499767998164.png)
Демистификация promise
Определение MDN для promise - это «возможное завершение (или сбой) асинхронной операции».
Определение выглядит так, как будто мы имеем дело с какой-то формой непонятной магии, но, проще говоря, promise
- это контейнер для будущей ценности.
promise
ведет себя в определенной степени как браузер, содержащий либо желаемое содержимое страницы, либо сообщение об ошибке. Всякий раз, когда мы переходим по адресу, ваш веб-браузер может находиться в трех разных состояниях:
- При извлечении данных может быть показана пустая страница или загрузочный счетчик.
- Содержимое страницы успешно отображено.
![](/static/storage/223762112521533767550200152131154550063.gif)
- Может появиться ошибка.
![](/static/storage/169190231409624954715805755435252579369.gif)
Promise может иметь три статуса:
pending
, когда, например, запрос не завершен;fulfilled
, когда сетевой запрос будет завершен;rejected
, когда асинхронный запрос завершится.
Практический пример статуса promise
Недавно созданное promise находится в состоянии pending
.
![](/static/storage/30603398921115758800279032854949199411.gif)
Аналогично, операция ввода-вывода (I/O), такая как запрос к базе данных, находится в pending
до тех пор, пока она не разрешится или не выдаст ошибку.
![](/static/storage/91360270672982149280878544280172175478.gif)
Мы можем прикрепить прослушиватель .then
/.catch
для обработки будущего значения promise
. Как только вызывается функция resolve
/reject
, promise меняет свой статус с pending
на fulfilled
/rejected
.
![](/static/storage/145544768272381746110805415191966358930.gif)
async/await
Современный JavaScript полностью основан на async
/await
, но эти два выражения являются альтернативным способом написания promise.
Мы можем переписать фрагмент базы данных с promise на async
/await
или наоборот.
![](/static/storage/34942767104131992612519928236759416594.png)
Мы можем перепутать синтаксис и сбить с толку наших коллег (крайне не рекомендуется для удобства чтения и согласованности).
![](/static/storage/339730294154522258374770634463977153532.png)
Promise под капотом
Promise, по-видимому, обрабатываются синхронно, но их выполнение откладывается до определенного момента в будущем.
Следующий сценарий показывает эту console.log
запускаются перед обратным вызовом promise.
![](/static/storage/42707451771659142103193004753821593110.gif)
Под капотом двигатель V8 запоминает функцию обратного вызова и помещает ее в очередь микрозадач, когда promise resolve
/reject
.
![](/static/storage/119880296625866858416231888991153637350.gif)
Как только стек вызовов опустеет, V8 сможет обрабатывать микрозадачи.
![](/static/storage/123780350243334128965665847150809557689.gif)
Очередь микрозадач Promise и цикл событий
Очередь микрозадач и цикл событий ожидают, пока стек вызовов опустеет, прежде чем отправлять задачи на выполнение, но какая из них имеет более высокий приоритет?
Следующий сценарий показывает, что Node.js и его внутренние компоненты всегда будут пытаться исчерпать очередь микрозадач, прежде чем передавать управление обратно циклу событий и его фазам:
setTimeout
и Promise.reject
ставят в очередь макрозадачу на этапе таймера и микрозадачу.
![](/static/storage/153044446720107570626045209200916637141.gif)
Обратный вызов в очереди микрозадач выполняется перед фазой таймера цикла событий. Как только микрозадач не останется, цикл событий может возобновить свое выполнение.
![](/static/storage/120074083639671657062643062987220264423.gif)
Обратный вызов setTimeout
вызывает setImmediate
и запускает другую микрозадачу promise
, которая будет выполнена в пустом стеке вызовов.
![](/static/storage/31683309344328332226078049419877398757.gif)
![](/static/storage/247494945725409135053419533309203975770.gif)
process.nextTick
process.nextTick
также планирует микрозадачи, которые Node.js управляется в выделенной очереди и имеет более высокий приоритет (в CommonJS), чем очередь promise и фазы цикла событий.
В следующем сценарии, Node.js помещает обратный вызов в их очередь.
![](/static/storage/93363299209396248046758635475985227804.gif)
Как только стек вызовов опустеет, Node.js обрабатывает микрозадачи process.nextTick
, за которыми следуют очередь promise
и фазы цикла событий.
![](/static/storage/32376542537615929951784051251854393115.gif)