Когда строки кода на любом языке программирования выполняются и завершаются в том порядке, в котором они встречаются, это называется синхронным потоком или поведением. Если вы считаете приведенный ниже список набором синхронно выполняемых строк кода, он всегда будет выполняться в одной и той же последовательности. Строка 2 будет выполнена и завершена только после завершения строки 1 и до начала строки 3:

print(“Line 1”)
print(“Line 2”)
print(“Line 3”)

Порядок, в котором появляются строки кода, не определяет порядок, в котором они завершают выполнение в асинхронном режиме. Строка 3 может быть завершена раньше строк 1 и 2. Строки 1 и 2 могут обрабатываться одновременно, однако строка 2 может быть завершена раньше строки 1.

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

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

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

Ключевые компоненты среды выполнения JS:

  1. Механизм JavaScript, который обрабатывает выполнение кода JS, состоит из следующих компонентов:
  • Куча памяти: раздел JS Engine, который выделяет память для переменных и функций выполняемого кода.
  • Стек вызовов. Однопоточный стек, в котором каждый контекст выполнения складывается по мере выполнения кода. Цикл обработки событий постоянно проверяет стек вызовов, представляющий собой очередь «последний пришел — первый обслужен» (LIFO), чтобы определить, нужно ли выполнять какие-либо функции. Он продолжает добавлять любые вызовы функций, обнаруженные во время выполнения, в стек вызовов и выполняет их в последовательности LIFO.

2. Веб-API доступны в браузере, но не в JS Engine. Асинхронные действия, такие как функция setTimeout(), методы манипулирования DOM, XMLHttpRequest для AJAX, геолокация и LocalStorage — все это часть среды выполнения.

3. Callback/Message Queue — это очередь, в которую помещаются callback'и, связанные с асинхронными вызовами, после завершения соответствующей асинхронной активности. Цикл событий выбирает обратный вызов для выполнения и помещает его для выполнения, когда стек вызовов пуст.

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

Теперь рассмотрим, как среда выполнения JavaScript, особенно стек вызовов, используется во время выполнения кода как для синхронного, так и для асинхронного кода:

const secondFunction = () => console.log(‘calling second function’);
const firstFunction = () => {
      console.log(‘start first function’);
      secondFunction;
      console.log(‘first function end’);
};
firstFunction();

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

  • Вызывается метод firstFunction, и стек вызовов обновляется первым контекстом выполнения.
  • Первоначальный журнал консоли выполняется как следующий оператор в стеке вызовов, записывается в журнал, а затем удаляется из стека.
  • Следующее предложение вызывает другую функцию, secondFunction(), которая создает новый контекст выполнения для secondFunction.
  • Журнал консоли добавляется в стек вызовов, запускается и удаляется из стека в первом операторе secondFunction.
  • Поскольку это конец secondFunction, контекст для secondFunction удаляется из стека.
  • Наконец, выполняется последний консольный оператор firstFunction, и firstFunction завершается и удаляется из стека вызовов.

Вывод консоли выглядит следующим образом:

start first function
calling second function
first function end

Теперь мы заставляем ту же функцию вести себя асинхронно, вызывая задержку с помощью setTimeout в secondFunction, как показано ниже:

const secondFunction = () => setTimeout(() => console.log(‘calling second function’), 1000);
const firstFunction = () => {
      console.log(‘start first function’);
      secondFunction;
      console.log(‘first function end’);
};
firstFunction();

Цикл событий из стека вызовов использовался для выполнения синхронного кода. Другие аспекты среды выполнения JS, такие как WebAPI и очередь сообщений, также задействованы в асинхронном программировании.

Давайте пройдемся по приведенным ниже шагам, причем первые несколько этапов останутся прежними, потому что в firstFunction нет изменений:

  • Вызывается метод firstFunction, и стек вызовов обновляется первым контекстом выполнения.
  • Первоначальный журнал консоли выполняется как следующий оператор в стеке вызовов, записывается в журнал, а затем удаляется из стека.
  • Следующее предложение вызывает другую функцию, secondFunction(), которая создает новый контекст выполнения для secondFunction.
  • В secondFunction есть setTimeout, равный 1 секунде (1000 мс). Функция JavaScript setTimeout одновременно обрабатывается браузером. При использовании метода setTimeout() создается новый контекст выполнения, который добавляется на вершину стека. Таймер создается вместе с обратным вызовом и находится в другом потоке в веб-API, где он работает асинхронно в течение 1 секунды, не мешая основному потоку кода.
  • Метод setTimeout() возвращает значение, и стек вызовов очищается.
    Метод secondFunction() аналогичен.
  • Выполняется последний оператор консоли firstFunction, и firstFunction завершается и удаляется из стека вызовов.
  • Через 1 секунду таймер удаляется из веб-API, а соответствующая функция обратного вызова перемещается в очередь сообщений. Он находится там до тех пор, пока стек выполнения не станет пустым, после чего он берется в цикл обработки событий.
  • Цикл событий будет постоянно следить за очередью сообщений и стеком выполнения. Когда стек выполнения пуст, он перемещает обратный вызов из очереди сообщений в него. В результате устанавливается контекст выполнения обратного вызова, а журнал консоли выполняется, завершается и удаляется из стека.

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

start first function
first function end
undefined
calling second function

Обратные вызовы

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

const asyncResponse = callback => { 
     setTimeout(() => callback (‘Response from asynchronous  function’), 1000);           
}

Асинхронная функция в приведенном выше примере — это функция asyncResponse, которая ждет 1 секунду перед отправкой ответа. Он принимает в качестве аргумента функцию обратного вызова, которая выполняется с ответом:

asyncResponse (response => console.log(response))

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

function logAsyncResponse(response) {
     console.log(response); 
} 
asyncResponse(logAsyncResponse);

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

Обещания

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

«Промис — это объект, который используется в качестве заполнителя для конечных результатов отложенных (и, возможно, асинхронных) вычислений».

Промис можно использовать для отслеживания задания, которое, как ожидается, займет некоторое время и, наконец, даст ответ. Обещание похоже на истинное обещание, которое приводит к ответу всякий раз, когда оно доступно.
Конструктор Promise используется для создания обещания, как показано ниже.

const myPromise = new Promise((resolve, reject) => {
     if (Math.random() * 10 <= 5) {
         resolve('Promise success!');
     }
     reject(new Error('Promise failed'));
});

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

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

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

  • resolve(value): эта функция используется для передачи успешного ответа на промис. Это установит статус обещания на выполнено. Значение, предоставленное как часть этой функции, будет установлено как обещанное значение.
  • reject(error): эта функция используется для передачи ответа об ошибке обещания. Это установит статус обещания на отклонено. Ошибка, переданная в качестве параметра этой функции, будет установлена ​​в обещанное значение.

Чтобы использовать промис, нам понадобится два набора обработчиков: один для выполненного состояния и один для отклоненного состояния. Функция then() используется для использования промиса.

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

  1. then() принимает два обратных вызова:
  • Когда обещание разрешается или устанавливается, запускается первая функция обратного вызова.
  • Когда обещание отклонено, вызывается вторая функция обратного вызова.
myPromise.then(success => {
       console.log(success);
}, error => {
       console.log(error);
});

2. Следующие два аргумента являются необязательными и могут обрабатываться выборочно:

  • Обрабатывать только успех:
myPromise.then(success => {
       console.log(success);
});
  • Обрабатывать только ошибку:
myPromise.then(null, error => {
       console.log(error);
});

3. Цепочка catch для обработки ошибки:

myPromise.then(success => {
     console.log(success);
})
.catch(error => console.log(error));

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

Асинхронное ожидание

Промисы решили проблему с обратными вызовами, но код остался большим и запутанным. Async Await, совершенно новая асинхронная конструкция, представленная в JavaScript ES8, упрощает взаимодействие с промисами. Оба ключевых слова, async и await, помещаются по отдельности, но действуют в тандеме для обработки асинхронного поведения.

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

const asyncFunc = async () => return "Async Function"
asyncFunc().then((result => console.log(result) //Async Function

Он может явно возвращать обещание, как показано ниже:

const asyncFunc = async () => return Promise.resolve("Async Function")
asyncFunc().then((result => console.log(result) //Async Function

Это также может привести к ошибке, как показано ниже:

const asyncFunc = async () => return Promise.reject(new Error('This promise is rejected with error!'));
asyncFunc().catch(error => console.log(error));

Независимо от того, что находится внутри асинхронной функции, она либо неявно, либо явно возвращает обещание.

Ждите

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

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

const printLanguage = language => {
   const promise= new Promise( (resolve) => {
      setTimeout(() =>{
          console.log(language);
          resolve(language);
      }, 2000);
    }
    );
    return promise;
}

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

Следующий код демонстрирует совместную работу async и await:

async function getLanguage(){
    const firstLanguage = await printLanguage("JavaScript");
    const secondLanguage = await printLanguage("java");
    return secondLanguage;
}
getLanguage().then((response)=>{
     console.log(response);
});

После того, как первое обещание выполнено, выполнение перейдет к следующей строке для получения второго обещания, и так далее, пока не будет выполнено третье обещание. Это последовательное поведение, вызванное использованием await в асинхронном методе. Если мы не хотим, чтобы три вызова выполнялись по порядку, так как нет внутренней зависимости, мы можем сделать их параллельно. Мы можем добиться параллельной обработки асинхронных вызовов, используя Promise.all, как показано ниже:

async function getLanguage(){
    const [firstLanguage, secondLanguage] = await Promise.all([printLanguage("JavaScript"),printLanguage("java")]);
    return secondLanguage;
}
getLanguage().then((response)=>{
     console.log(response);
});

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