Обратный вызов, обещания, асинхронное ожидание. Мы используем эти ключевые слова при извлечении данных из API, при создании задержек или ожидании вызовов базы данных и т. д.
Но почему? Почему мы вообще поддерживаем асинхронные шаблоны?
Давайте выясним!

Что означает асинхронный?

По определению — это кусок кода, который начнется сейчас и закончится когда-то в будущем. Примером асинхронного кода может быть:

  • Сетевой запрос (обращение к API)
  • Таймер (SetTimeout/SetInterval)
  • Обещание и т.д.

По умолчанию JavaScript является синхронным языком, то есть он выполняет код построчно. Однако он начинает вести себя совсем по-другому, когда мы добавляем в смесь асинхронные ингредиенты. Давайте посмотрим на пример этого:

console.log('Frodo')
setTimeout(() => console.log('Sam'), 1000)
setTimeout(() => console.log('Pippin'), 0)
console.log('Merry')

Здесь мы используем setTimeout для имитации асинхронного вызова. Его цель — задержать выполнение кода внутри функции обратного вызова на указанный период времени.
Поскольку тайм-ауты вызывают задержки, глядя на приведенный выше фрагмент, вы можете подумать, что выполнение будет происходить следующим образом:

  • Печать «Фродо»
  • Подождите 1000 мс (1 секунду) и напечатайте «Sam»
  • Напечатайте «Пиппин» и, наконец,
  • Печать «Веселый»

Но произойдет вот что:

[LOG]: "Frodo"
[LOG]: "Merry"
[LOG]: "Pippin"
[LOG]: "Sam" // after waited for whole second

Странно, правда?
На самом деле произошло следующее:

  • Печатаем «Фродо»
  • Затем идет «Сэм». Здесь JavaScript видит тайм-аут и планирует его на потом
  • Затем приходит еще один тайм-аут с «Пиппином» и переназначают его.
  • Печатаем «Веселый»
  • И тогда запланированные вызовы появляются в порядке завершения. «Пиппин» печатается первым, так как на его выполнение ушло меньше времени (0 мс), чем на «Сэм» (1000 мс).

Мы видим, что эти тайм-ауты предназначены для планирования выполнения, а не для его приостановки.
На самом деле здесь происходит то, что JavaScript обрабатывает асинхронный код в конце, в то время как остальная часть кода выполняется. Как это возможно?

Потоки

Поток — это небольшой набор инструкций, предназначенных для планирования и выполнения ЦП.

Все начинается с процесса. Процесс может иметь один или несколько потоков. Каждый поток предназначен для выполнения одной задачи от начала до конца.

Представьте типичное серверное приложение, использующее многопоточность.

  • У нас есть веб-сервер, работающий в основном потоке.
  • В эту тему приходит запрос.
  • Первичный поток порождает другой поток и передает запрос вновь созданному потоку.
  • Если приходит другой запрос, первичный поток создаст другой поток и назначит ему новый запрос.
  • Когда один из запросов будет завершен, задача будет возвращена основному потоку, который затем вернет ответ клиенту. И предыдущий поток может взять на себя другие запросы.

Таким образом, основной поток может создавать новые потоки каждый раз, когда поступает новый запрос.

Теперь, когда мы поняли, зачем нам нужно несколько потоков, вы можете подумать, что JavaScipt использует тот же механизм для обработки данных. Но вот поворот сюжета: JavaScriptявляется однопоточным!

Но почему тогда setTimeout или вызов API не блокируют выполнение программы?

Среды выполнения

Прежде чем мы ответим на предыдущий вопрос, давайте сначала разберемся, что такое среды выполнения. Среда выполнения — это то, что позволяет нам писать JavaScript везде, будь то в браузере или на сервере. Мы обсудим две части:

  • JavaScript-движок
  • Веб-API

JS-движок

Engine отвечает за преобразование кода JavaScript в машинный код. Это то, что позволяет нам выполнять JavaScript на наших машинах.
Существует несколько движков, но наиболее известным из них является V8, так как он поддерживает Google Chrome, Opera и Microsoft Edge.

Стек вызовов (иногда называемый просто стеком) – это структура данных стека, в которой хранится информация об активных подпрограммах компьютерной программы.
По сути, это пространство, где размещен весь наш код JS. Также помогает нам отслеживать, где мы находимся в коде.

Стек работает так: каждая строка кода помещается в стек (снизу вверх) и извлекается (сверху вниз) при выполнении (LIFO). Последний оператор выполняется первым, затем предыдущий и так до тех пор, пока стек не станет пустым.
Мы можем убедиться в этом, запустив этот код в режиме отладки.

function sum(a,b) {
    const result = a + b
    return displayResult(result)
}
function displayResult(result) {
    console.log(result);
}
debugger
sum(2, 3)

Код сортируется в обратном порядке, как и ожидалось.

Все начинается с запуска функции main(), созданной движком, и ее целью является запуск нашего кода. Мы не записываемmain()вручную.
Как только стек вызовов опустеет, main() также будет извлечен.

Веб-API

Веб-API — это API, встроенные в среду выполнения и предоставляющие собственные функции, которые можно использовать в приложении JavaScript. К ним относятся:

  • Объектная модель документа (API браузера)
  • Fetch API (API браузера/сервера)
  • Файловая система (серверный API)
  • Workers API (API браузера/сервера)
  • и т. д.

Поскольку они не встроены в язык, а являются частью среды выполнения, они не обрабатываются потоком JavaScript.

Возвращаясь к вопросу о том, почему поток JavaScript не блокируется, когда мы выполняем асинхронный запрос, потому что такой запрос обрабатывается планировщиком (API), а не стеком вызовов (основным потоком).

Более того, когда JavaScript видит асинхронный вызов,

  • он будет перемещен из стека вызовов,
  • получить обработку с помощью API,
  • поместить в очередь задач (иногда называемую очередью обратного вызова), где все остальные асинхронные вызовы удерживаются до тех пор, пока стек вызовов (основной поток) не станет пустым
  • затем помещается в стек вызовов, как только запрос готов к выполнению, и извлекается из стека после его выполнения
    (мы увидим пример этого ниже).

Веб-API используют дополнительные заданные потоки для обработки событий, запросов API, тайм-аутов и т. д., не блокируя основной контекст выполнения.
Таким образом, все это обрабатывается потоками, находящимися внутри среды выполнения, вне основного потока или за пределами ядра JavaScript, если хотите.

Вот почему, когда мы нажимаем кнопку или совершаем сетевой вызов в нашем внешнем приложении, приложение все еще работает, и экран не останавливается во время обработки запроса. Он работает сзади, что позволяет нашему приложению оставаться интерактивным.

Синхронные задачи остаются в основном потоке (стеке вызовов) и выполняются первыми. Асинхронные проходят через другой процесс (к которому мы вернемся через минуту), а затем помещаются в стек и выполняются в потоке.
Вот почему мы видим, что «Фродо» и «Мерри» печатаются первыми, а «Сэм» и «Пиппин» появляются позже.

console.log('Frodo') // sync
setTimeout(() => console.log('Sam'), 1000) // async
setTimeout(() => console.log('Pippin'), 0) // async
console.log('Merry') // sync

Такое планирование задач происходит и на внутренней стороне JavaScript. Вот почему такие вещи, как:

  • Сетевые вызовы (REST API)
  • Тайм-ауты
  • Потоки
  • Операции ввода/вывода (чтение/запись в файлы/базы данных)
  • Незавершенные задачи ОС (прослушивание порта)

не блокируйте выполнение в основном потоке. Вот почему Node.js рекламировался как неблокирующий ввод-вывод.

В случае Node.js веб-API обрабатывается библиотекой Libuv. Libuv написан на C, который является многопоточным языком, а это означает, что Node.js использует потоки под капотом.
В случае с Deno используется библиотека Rust под названием Tokio.
А все остальное, например циклы, условия, объявления и т. д., выполняется в основном потоке JavaScript.

Очевидно, что здесь происходит много всего, поэтому вам может быть интересно, как управляется весь процесс?

Цикл событий

При первом запуске нашего приложения JavaScript создает один поток, один стек вызовов и один экземпляр цикла событий. Цикл событий отвечает за выполнение кода, сбор и обработку событий и выполнение поставленных в очередь подзадач.

Фазы цикла событий

Мы можем думать о цикле событий как о цикле, который постоянно вращается и выполняет различные операции на разных этапах:

  1. Таймеры. На этом этапе выполняются обратные вызовы, запланированные setTimeout() и setInterval().

2. Ожидаемые обратные вызовы: выполняются обратные вызовы ввода-вывода, отложенные до следующей итерации цикла.

3. Простой, Подготовка: используется только для внутренних целей.

4. Опрос. Получайте новые события ввода-вывода и выполняйте обратные вызовы, связанные с вводом-выводом, например
— обработчик API app.get('/', (req, res) => {})
— файловая система fs.readFile().

На этом этапе выполняются почти все обратные вызовы, за исключением таймеров и проверок.

5. Проверить: здесь вызывается setImmediate() обратных вызовов.

6. Закрыть обратные вызовы, например. socket.on('close', ...).

Мы можем визуализировать цикл событий следующим образом:

Каждый раз, когда поступает новый запрос, Event Loop определяет, к какой фазе он принадлежит, и перемещает его туда. Это важно знать, так как эти фазы выполняются по порядку.
Итак, если у нас есть Таймер(setTimeout()) и Обратный вызов ввода-вывода (fs.readFile()), даже если для их выполнения требуется одинаковое время, Таймер будет выполняться первым.

Примечание. Один цикл (вращение) цикла событий называется тиком.

Практические примеры

Давайте еще раз вернемся к нашему исходному примеру и попробуем понять, как все это работает за кулисами.

  • Мы начинаем с того, что в стеке появляются Фродо, Сэм, Пиппин и Мерри.

  • Затем мы разделяем асинхронные задачи и отправляем их в веб-API.

  • После этого выполняем консольные логи (Фродо и Мерри), затем вызываем main()

  • Как только стек вызовов станет пустым, мы начнем обработку веб-API. Таймаут «Пиппин» имеет короткую задержку, поэтому ставится в очередь достаточно быстро

  • Теперь запускается цикл событий. Он проверяет, есть ли что-нибудь в очереди задач. Если стек пуст, он берет первое из очереди и помещает его в стек

  • Затем выполняем первый таймаут, поступивший в стек. Но тут мы подходим к очень интересному моменту.
    Стек вызовов пуст, но время ожидания «Сэм» все еще обрабатывается.
    Цикл событий будет продолжать вращаться, проверять стек и стоять в очереди туда и обратно, ожидая появления новой задачи в очереди.

  • Наконец, второй тайм-аут поступает в очередь

  • И цикл событий помещает его обратно в стек, где он выполняется, а затем удаляется.

Но что произойдет, если у нас будет два тайм-аута и у обоих будет одинаковая задержка?

  • Оба будут отправлены в веб-API.

  • Затем поставить в очередь на завершение заказа

  • Затем один появится в стеке вызовов и будет выполнен. Потом другой.

В основном задачи выполняются в том порядке, в котором они были поставлены в очередь.
Это происходит потому, что цикл обработки событий может помещать в стек только одну задачу за раз.

Макрозадачи и микрозадачи

До сих пор мы в основном обсуждали обратные вызовы, но как насчет промисов? На каком этапе они выполняются?

ECMAScript 2015 представил концепцию очереди заданий, которая используется Promises, queueMicrotask и Mutation Observer API. Это способ выполнить результат асинхронной функции как можно быстрее, а не помещать его в конец стека вызовов.

Любая задача в очереди заданий (микрозадача) будет иметь более высокий приоритет, чем задача очереди обратного вызова (макрозадача).

Микрозадачи могут выполняться на каждом этапе цикла событий. На самом деле они имеют приоритет.

setTimeout(() => console.log('Callback executed'), 0)
Promise.resolve().then(() => console.log('Promise executed'))

Если у нас есть Callback и Promise в одном блоке, Promise выполнится перед Callback.

[LOG]: "Promise executed"
[LOG]: "Callback executed"

То же самое верно для process.nextTick()function в Node.js, которая имеет приоритет над промисами. Эта функция вызывается на каждом тике (вращении) цикла событий.

Promise.resolve().then(() => console.log('Promise logged'))
process.nextTick(() => {
  console.log('Next tick logged')
});
//
[LOG]: "Next tick logged"
[LOG]: "Promise logged"

Таким образом мы можем сообщить движку JavaScript обработать функцию асинхронно как можно скорее.

Не блокируйте цикл событий

Золотое правило JavaScript гласит: «Не блокировать цикл обработки событий».
По сути, это означает, что мы используем асинхронное программирование, когда это возможно, чтобы иметь более производительные приложения.

Любая синхронная задача будет блокировать цикл событий, поскольку цикл событий не может работать, пока стек вызовов не станет пустым. Но знаете ли вы, что промисы также могут блокировать цикл событий?

Промис, будучи микрозадачей, может ставить в очередь другие микрозадачи. Это означает, что промисы будут выполняться одно за другим, полностью игнорируя операции в других фазах.

Цикл событий не переходит к следующей задаче вне очереди микрозадач, пока не будут выполнены все задачи в очереди микрозадач.

setTimeout(() => { console.log('Timeout') }, 0)
Promise.resolve().then(() => { console.log('Promise 1') })
Promise.resolve().then(() => { console.log('Promise 2') })
Promise.resolve().then(() => { console.log('Promise 3') })
//
[LOG]: "Promise 1"
[LOG]: "Promise 2"
[LOG]: "Promise 3"
[LOG]: "Timeout"

Это проблема, потому что мы можем захотеть читать из файлов или выполнять другие операции ввода-вывода, которые управляются через обратные вызовы, но из-за того, что промис имеет более высокий приоритет, он блокирует цикл событий, и мы никогда не дойдет до того, чтобы приступить к обработке других задач.

Чтобы решить эту проблему, мы можем обернуть код обратного вызова в промис или найти альтернативы API, которые используют промисы, например.

const fs = require('fs').promises;
fs.readFile('/file.txt')
  .then((result) => console.log(result))
  .catch((err) => console.error(err));

Или избегайте использования микрозадач там, где они могут стать узким местом.

Вложенные операции

Теперь мы знаем, что если мы поместим Promise и Callback под одним зонтиком, Promise будет иметь приоритет над Callback. Чтобы действительно уложить это в постель, давайте еще раз посмотрим на вложенный код.

setTimeout(() => {
  Promise.resolve().then(() => console.log('Promise 1'))
  setTimeout(() => {
    console.log('Timeout 1')
  }, 0)
  console.log('Logger 1')
}, 1000)
setTimeout(() => {
  setTimeout(() => {
    console.log('Timeout 2')
  }, 0)
  Promise.resolve().then(() => console.log('Promise 2'))
  console.log('Logger 2')
}, 1000)
Promise.resolve().then(() => console.log('Promise 0'))

В этом случае на выходе будет:

[LOG]: "Promise 0"
[LOG]: "Logger 1"
[LOG]: "Promise 1"
[LOG]: "Logger 2"
[LOG]: "Promise 2"
[LOG]: "Timeout 1"
[LOG]: "Timeout 2"
  • При запуске приложения никаких блокирующих задач нет, поэтому вызываем первый промис, так как он имеет приоритет над таймаутами.
  • Затем мы вызываем первый тайм-аут.
    Код внутри не сразу доступен из-за задержки (1000 мс), поэтому цикл обработки событий переходит к следующему тайм-ауту.
  • Второй тайм-аут также имеет задержку.
    Что будет делать цикл событий, так это сделать несколько вращений (тиков), пока не истечет любой из таймеров.
  • По завершении цикл событий просматривает первый тайм-аут и определяет регистратор, микрозадачу и макрозадачу. Первым выполняется Logger, за которым следует Promise (микрозадача) в той же области.
  • На том же уровне, прямо за пределами первой оболочки, у нас есть еще один тайм-аут, который также имеет регистратор, микро- и макрозадачу.
  • Распечатывается второй регистратор, за которым следует вторая микрозадача.
  • Наконец, мы добрались до макрозадач, так как они имеют наименьший приоритет. Итак, мы называем их обоих.

Очередь Микрозадача

Queue Microtask — это новый API браузера, который запускается после того, как текущая задача завершила свою работу и когда нет другого кода, ожидающего запуска, прежде чем управление контекстом выполнения вернется в цикл событий браузера.

Все, что мы положим внутрь, будет обработано в первую очередь.

queueMicrotask(() => {
  console.log('Hello World');
});

Это похоже на то, что мы делали с помощью setTimeout:

setTimeout(() => {
  console.log('Hello World');
}, 0);

Единственное отличие состоит в том, что queueMicrotask будет выполняться с опережением из-за наличия очереди микрозадач.

Демистификация Async Await

Функция Async Await была добавлена ​​в JavaScript вместе с ECMAScript 2017, чтобы лучше обрабатывать промисы. Но есть распространенное заблуждение по этому поводу.

Существует мнение, что если вы поместите ключевое слово async перед вызовом функции в JavaScript, вы превратите операцию, которая обычно является синхронной, в асинхронную. Хотя в этом нет ничего плохого, это определенно не меняет правила игры, как кажется.
Позвольте мне показать вам, что я имею в виду.

В C# вызовы API синхронны по умолчанию, что не является большой проблемой, поскольку C# является многопоточным.

public List<UserClass> GetUsers(int userId)
{
  var result = _userContext.GetUsers()
    .Where(...)
    .ToList();
  return result;
}

Мы можем преобразовать это в асинхронный вариант, используя класс Task.

public async Task<List<UserClass>> GetUsers(int userId)
{
  var result = await _userContext.GetUsers()
    .Where(...)
    .ToListAsync();
  return result.Value;
}

Можно было бы подумать, что JavaScript работает так же, но мы только что узнали, что JavaScript может выполнять асинхронные задачи в обратных вызовах (макрозадачах), которые были частью языка задолго до Async Await.

На самом деле, JavaScript/Node.js всегда будет пытаться выполнять операции ввода-вывода асинхронно, если только мы специально не укажем иное.

const fs = require('fs');
fs.readFile( path, options ) // Asynchronous
fs.readFileSync( path, options ) // Synchronous

Так в чем же тогда цель Async Await в JavaScript?

Async Await — это оболочка для промисов — способ обрабатывать их более читабельным образом и проще в обслуживании.

Нативное использование промисов

function greeting() {
  const greetingPromise = Promise.resolve('Hello World')
  greetingPromise.then(greet => console.log(greet))
}
greeting();

Альтернатива асинхронному ожиданию

async function greeting() {
  const greetingPromise = Promise.resolve('Hello World')
  const greet = await greetingPromise;
  console.log(greet);
}
greeting();

Это работает следующим образом: мы помещаем ключевое слово await перед ожидающим обещанием. Это работает как вызов Promise.then(), за исключением того, что для получения вывода (приветствия) не требуется обратный вызов.

Давайте быстро пройдемся по всем шагам:

  • Чтобы использовать await внутри функции, она должна быть объявлена ​​с ключевым словом async
  • Ключевое слово await может быть помещено только перед выражением, результатом которого является обещание.
  • Ключевое слово await приостанавливает выполнение функции async до тех пор, пока обещание не будет выполнено (разрешено или отклонено).
    Когда это происходит, весь await оценивается как значение результата Promise, а затем возобновляется выполнение функции async.

Несмотря на то, что Async Await делает наш код синхронным, имейте в виду, что это все еще оболочка для Promise, который является асинхронным. Однако есть небольшое отличие — Пауза.

function greetingWithPromises() {
  console.log('Logger 1')
  const greetingPromise = Promise.resolve('Hello World')
  greetingPromise.then(greet => console.log(greet))
  console.log('Logger 2')
}
greetingWithPromises();
// 
[LOG]: "Logger 1"
[LOG]: "Logger 2"
[LOG]: "Hello World"

Давайте сравним это с Async Await.

async function greetingAsyncAwait() {
  console.log('Logger 1')
  const greetingPromise = Promise.resolve('Hello World')
  const greet = await greetingPromise;
  console.log(greet);
  console.log('Logger 2')
}
greetingAsyncAwait();
// 
[LOG]: "Logger 1"
[LOG]: "Hello World"
[LOG]: "Logger 2"

Поскольку выражение await заставляет выполнение асинхронной функции приостанавливаться до тех пор, пока промис не будет выполнен, await не позволит регистраторам для завершения перед продолжением выполнения. По сути, Async Await выполняется до Promise.then(), поскольку ему не нужно ждать, пока стек вызовов опустеет.

Последний замечание с асинхронными функциями в JavaScript — они всегда возвращают промисы, независимо от того, что они содержат.

Затем вывод можно обработать с помощью другого await или Promise.then().

Async Await нашел свое применение и в других ветках.

Файловая система

Как и в этом примере чтения данных из файла.

const fs = require('fs').promises;
const output = await fs.readFile( path, options )

Обработчики API (Express.js):

До

app.get('/', (req, res) => { // call to database .then( ... ) })

После

app.get('/', async (req, res) => { // await call to database })

Краткое содержание

JavaScript делегирует асинхронные задачи из основного потока веб-API. Вот почему асинхронные запросы не блокируют выполнение программы.

То же самое относится и к серверной части JS. DNS-запросы являются асинхронными. Когда приходит новый запрос, он делегируется веб-API. Вызовы БД (операции ввода-вывода) также не блокируются, поэтому основной поток может принимать другие запросы, в то время как потоки Liuv обрабатывают запросы ввода-вывода сзади.

Цикл событий управляет планированием задач. Он состоит из нескольких этапов, каждый из которых отвечает за обработку различных операций.

У нас есть очереди микрозадач и макрозадач. С каждым из них связан ряд API.

Микрозадачи имеют приоритет и поэтому выполняются заранее, но блокируют цикл обработки событий.
Async Await — это оболочка для функций Promise, которая позволяет нам писать асинхронный код синхронным образом.

За и против

Теперь давайте рассмотрим плюсы и минусы использования асинхронного кода.

Минусы

Скорость
Асинхронные вызовы на самом деле медленнее, чем синхронные, потому что для их обработки требуется время, а также для прохождения туда и обратно по циклу событий.

Разработка
Написание и отладка асинхронного кода — сложная задача. Возможно, вы слышали о термине Callback-Hell. Это ряд глубоко вложенных функций, которые зависят друг от друга и очень сложны для чтения и обслуживания.

const fs = require('fs');

const data = 'Hello World';  
setTimeout() => {
  fs.readFile(fileWithData, 'utf8', (err, response) => {  
    if (err) return console.log(err);

    fs.writeFile(data, response, (err) => {
        if(err) return console.log(err);
        console.log('Done!');
    });
  });
}, 0);

У более новых API есть и недостатки.
Поскольку функции, помеченные ключевым словом async, возвращают обещание, если у нас есть ряд вложенных вызовов функций, каждый из них должен быть помечен как async, чтобы иметь возможность использовать внутри ключевое слово await. В противном случае мы склонны использовать промисы.

function changeGear() {
  return Promise.resolve('Fifth Gear');
}
async function hitTheGas() {
  return await changeGear();
}
async function drive() {
  const data = await hitTheGas();
  console.log(data); // Fifth Gear
}
drive();

Синхронные операции
Мы знаем, что синхронные операции выполняются в основном потоке. Поскольку JavaScript использует только один поток для обработки этих операций, наше приложение будет выполняться строка за строкой, и когда оно сталкивается с циклом, оно приостанавливается.

Вот почему JavaScript не подходит для длительных блокирующих операций, таких как обработка чисел. Поскольку все блокирующие операции выполняются в одном потоке, длительные операции нельзя делегировать другим потокам, а это означает, что наше приложение будет ждать и блокировать все остальное.

Это, вероятно, самый большой недостаток JavaScript, но недавно в язык были добавлены некоторые возможные решения, такие как Web Workers API.

Плюсы

Улучшения API
Асинхронный API постоянно совершенствуется с новыми версиями ECMAScript. Все началось с обратных вызовов, затем перешло к обещаниям, а затем к асинхронному ожиданию.
Недавно в Promise API появились новые дополнения, такие как Promise.any(), Promise.race() и Promise.all().
Кроме того, есть также ожидание верхнего уровня.

Extended Toolkit
JavaScript поставляется со списком инструментов для работы с асинхронными данными, такими как:

  • XMLHttpRequest
  • АЯКС
  • Получить API

а также сторонние:

  • Аксиос
  • Rx.js
  • Async.js

Динамический импорт модулей
Вместо импорта поверх файла модули можно импортировать динамически. Идея этого заключается в использовании асинхронных вызовов для импорта модулей (файлов) после выполнения определенного действия, например нажатия кнопки.

Неблокирующее выполнение
Как мы уже установили, асинхронный код обрабатывается вне основного потока и, таким образом, не блокирует поток.

Тем не менее, теперь мы знаем, почему важно использовать асинхронные шаблоны в JavaScript, когда это возможно. Многие популярные фреймворки и библиотеки в NPM автоматически используют шаблоны Async, хотя некоторые из старых по-прежнему работают только с обратными вызовами. Так что это и хит, и промах.

Рабочий API

Прежде чем закончить, скажем пару слов о новом JavaScript Workers API. Веб-воркеры позволяют выполнять операцию сценария в фоновом потоке отдельно от основного потока выполнения приложения JavaScript.

Его цель — разделить задачи синхронизации на более мелкие рабочие процессы, чтобы длительные операции (например, циклы) не блокировали основной поток.

Это работает так, как мы и ожидали. Основной поток JavaScript может отправлять данные рабочим потокам. Каждый рабочий процесс обрабатывает задачу, отправленную основным потоком, и возвращает ее после завершения.

В Node.js также есть свой вариант с использованием встроенного модуля рабочие потоки.

Однако они неэффективны для асинхронных задач. Стандартный (Event Loop) способ намного лучше справляется с асинхронными задачами.

Улучшенный пул потоков

Даже со всеми возможностями среды выполнения мы по-прежнему ограничены размером пула потоков. Библиотека Libuv позволяет нам обрабатывать только несколько операций ввода-вывода одновременно, так как Libuv имеет до 4 потоков в пуле потоков.

Мы можем увеличить это число до 1024 потоков, установив следующую переменную среды при запуске приложения:

UV_THREADPOOL_SIZE = x

Но имейте в виду, что больше не всегда лучше. Попробуйте найти оптимальный размер пула потоков в соответствии с производительностью вашей машины. Обычный подход — установить его равным количеству логических ядер вашего ПК.

Мы можем убедиться в этом, импортировав модуль ОС (родной для Node.js), затем вызвать os.cpus() (который возвращает массив) и распечатать его длину:

const os = require('os')
console.log(os.cpus().length) // e.g. 16

Затем мы можем установить размер пула потоков либо в файле .env, либо динамически (в нашем основном файле JS):

process.env.UV_THREADPOOL_SIZE = os.cpus().length

Заключительные слова

Чтобы связать вещи бантиком, JavaScript использует асинхронный код для выполнения вне основного потока. Этот шаблон хорош тем, что не блокирует выполнение программы, но имеет и некоторые недостатки.

В целом, это удивительная функция, которая выделяет JS из толпы.

В части 2 мы углубимся в практические примеры с Fetch API, Observables, обработкой ошибок и многим другим.

Полезные ресурсы: