Если JavaScript является однопоточным, как работают асинхронные функции?

Отказ от ответственности: эта статья в значительной степени вдохновлена ​​докладом на конференции, сделанным Филиппом Робертсом на EUConf 2014.

Критики JS любят указывать на то, что он однопоточный. Когда я начал кодировать на JS, это меня смутило. Тогда как JS обрабатывал асинхронные функции? Как мы можем иметь такие вещи, как setTimeout(), которые, кажется, работают независимо от основного потока? Надеюсь, к концу статьи вы получите ответы на все эти вопросы. Другая цель этой статьи - объяснить некоторые основные передовые концепции JS: стек вызовов, очередь задач и цикл событий, которые имеют основополагающее значение для понимания того, как JS работает в браузере.

Допустим, вы написали код JS для веб-приложения и отправили его. Этот код JS запускается в браузере. Как браузер узнает, что с ним делать? Браузер может запускать JS-код, потому что у него есть JS-движок, который для Chrome является движком JavaScript V8. Проще говоря, V8 умеет преобразовывать JS-код в машинный код.

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

Браузер может запускать JS-код, потому что в нем есть JS-движок, который для Chrome является движком JavaScript V8.

Мы рассмотрели две важные части браузера: движок JS и веб-API. Теперь перейдем к однопоточной природе JS. Когда мы говорим, что JS является однопоточным, мы имеем в виду, что JS имеет один стек вызовов. Стек вызовов - это простая структура данных, которая имеет две функции: она может добавлять (выталкивать) или удалять (выталкивать) элементы только сверху. Представьте ведро, в которое вы складываете посуду после того, как закончите мытье. Вы можете добавлять или удалять блюдо только сверху. Когда функция вызывается, она добавляется в стек вызовов, а при возврате отключается. Вот как JS отслеживает, какие функции и когда запускать. Наглядный пример:

Когда мы говорим, что JS является однопоточным, мы имеем в виду, что JS имеет один стек вызовов. Стек вызовов - это простая структура данных, которая имеет две функции: она может добавлять (выталкивать) или удалять (выталкивать) элемент только сверху.

Учитывая, что JS имеет только один стек вызовов, функции в нижней части стека зависят от функций над ними: они не будут запущены, пока все функции над ними не закончат выполнение. Если мы поместим в стек асинхронную функцию, которая выполняет сетевой вызов, она заморозит браузер до тех пор, пока не будет завершена, поскольку сетевые вызовы могут быть сравнительно медленными. Это сделает пользовательский опыт невыносимым. Итак, как нам решить эту проблему? Войдите в очередь задач.

Картина говорит тысячу слов:

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

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

Интересный лакомый кусочек: если вы хотите, чтобы фрагмент кода запускался при пустом стеке вызовов, оберните его в setTimeout () с задержкой в ​​0 секунд. Несмотря на то, что он равен 0, он будет извлечен циклом событий из очереди задач и помещен в стек вызовов, когда стек пуст.

Интересный лакомый кусочек 2: Очереди заданий зарезервированы для обещаний. Готовый код обещаний хранится в очередях заданий.

Подведем итоги:

  • JS является однопоточным, то есть имеет только один стек вызовов.
  • Когда функции вызываются, они помещаются в стек вызовов. Когда они возвращаются / завершаются, их снимают.
  • Асинхронные функции не являются частью движка JS. Они доступны через веб-API. Когда вы используете один, то есть setTimer (callback, timeInMs), он помещается в стек вызовов, запускает метод, то есть таймер (callback, timeInMs) в веб-API, и извлекается из стека вызовов, освобождая стек вызовов.
  • Когда функция, запускаемая асинхронным вызовом в стеке веб-API, выполнена, то есть таймер (обратный вызов, timeInMs), функция обратного вызова асинхронного метода помещается в очередь обратного вызова.
  • Задача цикла событий - переместить данные из очереди обратного вызова и обратно в стек вызовов. Они делают это только тогда, когда стек вызовов пуст.