С тех пор, как я покинул мой Flatiron Bootcamp, магия промисов Javascript заинтриговала меня и озадачила одновременно.

Иногда даже понимание асинхронности и синхронности оказывалось сложным. Не потому, что я не понимаю, что они означают, а скорее потому, что я часто оказываюсь в ловушке этого мышления, думая о предмете более глубоко, чем это требуется для вашей стандартной концепции async/await или вашей базовой цепочки .then() .catch() для выполнения простого запроса API.

Я ловлю себя на том, что думаю о многопоточном характере кода, на котором фактически работает код, о том, как блокировка кода влияет на использование памяти в фоновом режиме, как setTimeout() влияет на цикл обработки событий. Все эти вопросы заставляют вас углубиться в довольно глубокие темы, такие как движок Google V8 и среда выполнения Javascript, что очень обескураживает, когда все, что я собирался сделать, это зайти на StackOverflow и увидеть пример обещания JavaScript.

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

Асинхронный и синхронный код

Асинхронный — это сокращение от «асинхронный». Легче понять асинхронность, если вы сначала поймете, что означает «синхронный» наоборот.

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

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

Функция setTimeout(), вероятно, является самым простым способом асинхронного планирования выполнения кода в будущем:

Что делает функция setTimeout(), так это устанавливает событие (тайм-аут), которое должно произойти позже.

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

Однопоточное ограничение

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

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

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

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

Понимание цикла событий

До ES6 в самом JavaScript никогда не было встроенного прямого понятия об асинхронном коде. Движок JavaScript никогда не делал ничего, кроме выполнения одного фрагмента вашей программы в любой момент времени.

Так что же говорит движку JS выполнять блоки кода, которые вы написали? Движок работает в среде хостинга, проще говоря, это веб-браузер или Node.js.

Общим знаменателем во всех средах является встроенный механизм, называемый циклом событий, который обрабатывает выполнение нескольких фрагментов вашей программы с течением времени, каждый раз вызывая JS Engine. Движок JS — это просто служба, которая просто выполняет по требованию произвольный код JS в среде.

Так что же такое цикл событий?

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

Каждый цикл цикла событий называется тиком. Каждое событие — это просто обратный вызов функции.

Работа в ES6

В ES6 была введена новая концепция под названием «Очередь заданий». Это слой поверх очереди Event Loop.

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

Вложенные обратные вызовы

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

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

Обещания

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

Обещанная ценность

Давайте посмотрим, как мы можем реализовать концепцию обещаний в примере x + y из предыдущего примера:

  1. fetchX() и fetchY() вызываются напрямую, а возвращаемые ими значения (promises) передаются в sum > функция. Эти значения X и Y всегда представлены какбудущие значения, независимо от того, готовы они в данный момент или нет.
  2. Функция sum() возвращает Promise.all(), который создает промис (который ожидает promiseX и promiseY решить).
  3. Мы ждем разрешения обоих промисов, связывая .then() в конце, что фактически выполняет добавление значения X и значения Y.
  4. Как только функция sum завершает сложение двух значений, она также возвращает обещание. Который мы можем использовать, привязав другой .then() к его концу, чтобы console.log() получить результирующую сумму.

В промисах вызов then(...) фактически может выполнять две функции: первую для выполнения (как показано ранее), а вторую для отклонения:

Если что-то пошло не так при получении x или y или что-то пошло не так во время добавления, обещание, которое возвращает sum(), будет отклонено, и второй обработчик ошибок обратного вызова, переданный в .then(), получит значение отклонения из обещания.

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

Вывод

В своей самой базовой форме JavaScript — это синхронный, блокирующий, однопоточный язык, в котором одновременно может выполняться только одна операция. Но ES5 определяет функции и API, которые позволяют нам определять функции, которые не должны выполняться синхронно, а вместо этого должны вызываться асинхронно, когда происходит какое-то событие (течение времени, взаимодействие пользователя с мышью или поступление данных через сети, например). Это означает, что вы можете позволить своему коду делать несколько вещей одновременно, не останавливая и не блокируя основной поток.

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