Э… что?

Недавно я участвовал в нескольких интервью с фронтенд-разработчиками. К сожалению, печальная правда заключается в том, что большинство кандидатов не понимают, как работают «асинхронные» вещи в JavaScript, что считается блокирующим или неблокирующим кодом и как с этим связана (а)синхронность.

Давайте обобщим самые распространенные заблуждения и проясним их.

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

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

А как насчет вышеупомянутого асинхронного кода?

Асинхронные коды обычно запускаются после события, например ответа сети, или после определенной задержки. В двух словах: движок JS добавляет эти так называемые «обратные вызовы» (все обратные вызовы, промисы и микрозадачи) в определенные очереди событий (промисы и микрозадачи имеют отдельные очереди), затем запускает их после обработки текущих функций на главном нить.

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

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

Давайте рассмотрим несколько примеров сценария блокировки взаимодействия с пользователем. В следующем примере вы увидите два вращающихся текстовых элемента: CSS управляет поворотом одного из них, на который не повлияет код блокировки, поскольку браузер запускает его совершенно отдельно. Сценарий поворачивает другой, а простой обратный вызов «setTimeout» изменяет угол текста каждые 100 мс. Вы увидите, что этот сценарий ротации будет заблокирован, когда основной поток будет занят длительной операцией, запущенной пользовательской активностью.

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

Некоторые примеры:

  • обещать
  • асинхронная функция
  • обратный вызов setTimeout

В следующем примере мы выполнили одну и ту же длительную задачу с асинхронной функцией и обратным вызовом «setTimeout» — оба блокируются…

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

Идти параллельно

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

Это возможно с помощью API веб-воркеров современных веб-браузеров, а также возможно в node.js, но с другим API.

Теперь давайте посмотрим в нашем последнем примере, блокируем ли мы пользовательский интерфейс… и нет!

Заверните

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

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