Асинхронные задачи на синхронном языке

JavaScript часто называют синхронным однопоточным языком. По сути, это означает, что он может выполнять только одну функцию за раз, и если для выполнения функции требуется некоторое время, например HTTP-запрос, он буквально блокирует взаимодействие пользователя с веб-страницей. Например, события щелчка будут отложены до окончательного завершения функции блокировки. Конечно, это будет плохой пользовательский опыт. Итак, как именно с этим работает JavaScript? Как он может выполнять асинхронные задачи? «Функции обратного вызова!» "Обещания!" "Асинхронный!" Ага, вы поняли. Думаю, больше нечего сказать… Если не считать шуток, JavaScript, опять же, является синхронным языком. Чтобы понять, как он может выполнять асинхронные задачи, нам нужно взглянуть на то, что происходит «под капотом» браузера.

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

Стек вызовов управляет выполнением вызываемых функций с использованием структуры «последним вошел - первым ушел». После запуска сценария первая функция помещается в стек вызовов. Если первая функция вызывает вторую функцию, то она накладывается поверх нее. Допустим, вторая функция вызывает console.log('hello world');. Это будет помещено поверх второй функции. Если нет других вызываемых функций, console.log выполняется и после завершения извлекается из стека. Затем выполняется вторая функция, которая также отключается. Наконец, первый ведет себя так же, оставляя стек вызовов пустым. Это однопоточный язык в действии.

Но что произойдет, если для выполнения одной из этих функций потребуется время? Скажем, вместо того, чтобы вторая функция выполняла console.log, вместо этого она выполнила setTimeout, выполнение которой заняло пять секунд. Это по существу предотвратит выполнение остальных функций в стеке вызовов до его завершения. Это называется функцией блокировки и приводит к плохому взаимодействию с пользователем. Здесь вступает в действие остальная часть порядка выполнения. Когда механизм выполнения JavaScript достигает асинхронной функции в стеке вызовов, он немедленно возвращается и выскакивает из стека. Пока стек вызовов переходит к следующей функции, веб-API обрабатывают асинхронную функцию. Именно благодаря веб-API, встроенным в наши браузеры, JavaScript может выполнять асинхронные задачи. Функция обратного вызова и любые связанные с ней метаданные регистрируются в таблице событий, а затем передаются в очередь задач. Например, возьмем следующее:

setTimeout(function() {
  console.log('Logged after a 1 second delay');
}, 1000);

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

Очередь задач работает по принципу «первым пришел - первым обслужен». Первая функция должна дождаться прохождения разрешения цикла обработки событий. Цикл событий - это алгоритм, постоянно проверяющий стек вызовов, и не пропускает ни одну из функций обратного вызова в стек, пока он не станет пустым. Как только это произойдет, он возьмет первый обратный вызов из очереди задач и поместит его в стек вызовов, позволяя ему выполнить. Как только стек снова станет пустым, цикл обработки событий разрешит следующий обратный вызов из очереди в стек и будет повторять этот процесс до тех пор, пока сама очередь не станет пустой.