JavaScript — это язык сценариев, в основном основанный на концепции однопоточного выполнения.

Что такое однопотоковое выполнение?

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

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

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

Зачем нужны асинхронные операции и какие преимущества они дают?

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

В JavaScript есть несколько способов добиться этого синтаксически:

  • Обратные вызовы
  • Обещания
  • Асинхронно и ждать

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

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

function asyncFunction(callback) {
  console.log("Operation started.");

  setTimeout(function() {
    console.log("Operation completed.");
    callback();
  }, 2000);
}

function callbackFunction() {
  console.log("Callback function called.");
  console.log("Operation finished, you can proceed.");
}

// Calling asyncFunction and passing callbackFunction as an argument
asyncFunction(callbackFunction);

console.log("Program continues...");

В этом примере у нас есть функция с именем asyncFunction. Эта функция ждет 2 секунды, а затем завершает операцию, после чего вызывается указанная функция обратного вызова. Функция setTimeout задерживает операцию на 2 секунды, а затем выполняет функцию обратного вызова, когда операция завершена.

callbackFunction — это функция обратного вызова в этом примере, которая просто выводит некоторые сообщения на консоль.

Вывод кода будет таким:

Operation started.
Program continues...
Operation completed.
Callback function called.
Operation finished, you can proceed.

Давайте подойдем к тому же сценарию, используя промисы.

Обещания

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

Когда операция успешно завершена, вызывается resolve, а в случае ошибок вызывается reject.

function asyncFunction() {
  console.log("Operation started.");

  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log("Operation completed.");
      resolve();
    }, 2000);
  });
}

function callbackFunction() {
  console.log("Promise resolved successfully.");
  console.log("Operation finished, you can proceed.");
}

// Calling asyncFunction and using the returned Promise with then() and catch() methods
asyncFunction()
  .then(callbackFunction)
  .catch(function(error) {
    console.log("Promise rejected with error: " + error);
  });

console.log("Program continues...");

В дополнение к упомянутой выше информации метод then используется для указания функции обратного вызова, которая будет выполняться при успешном разрешении промиса. С другой стороны, метод catch используется для указания функции обратного вызова, которая будет выполняться, когда промис отклонен с ошибкой.

Вывод кода будет таким:

Operation started.
Program continues...
Operation completed.
Promise resolved successfully.
Operation finished, you can proceed.

«В этом случае выполнение программы продолжается сразу после вызова asyncFunction, и на консоль выводится сообщение «Программа продолжается…». Через 2 секунды операция завершается и вызывается resolve. Следовательно, выполняется функция callbackFunction, указанная в методе then, и печатаются сообщения«Promise успешно выполнен .” и "Операция завершена, вы можете продолжить." на консоль.

Однако код кажется немного длинным и сложным, верно? В JavaScript всегда есть более чистый способ написания кода :)

Теперь мы подошли к основной теме нашего обсуждения — структуре Async & Await.

Асинхронно и ждать

В любом случае Async & Await помогает нам выполнять асинхронные операции, такие как обратные вызовы и обещания. По структуре синтаксиса они не сильно отличаются от привычных нам функций. MDN — асинхронная функция

async function fetchData() {
  // Asynchronous code locates here
}

Примечание. Как и обычные функции, асинхронные функции также могут принимать параметры таким же образом.

Как мы знаем об обещаниях, успех или неудача операции определяется ключевыми словами разрешить и отклонить, и нам нужно продолжить процесс, используя .then(), если операция прошла успешно, или .catch() если это не удается.

И здесь приходит на помощь await.

Ключевое слово await используется для приостановки выполнения функции до тех пор, пока Promise не будет разрешено, и оно может только использоваться внутри асинхронной функции. МДН — Жду

async function fetchPostsData() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');
    const data = await response.json();
    return data;
  } catch (err) {
    console.error('Error of fetching posts:', err);
    throw err;
  }
}

Источник API: https://jsonplaceholder.typicode.com/

В этом примере мы использовали async/await для получения пользовательских данных из API, анализируя ответ как JSON. , и обрабатывать любые потенциальные ошибки, которые могут возникнуть во время процесса.

Кроме того, для обработки любой ошибки, которая может возникнуть в результате Promise, мы обернули всю операцию внутри блока try/catch. Позже мы углубимся в эту тему более подробно.

Давайте рассмотрим, почему мы предпочитаем работать с async/await вместо обратных вызовов, на простом примере.

Callback Hell против Async/Await

Обратный ад

function getData(callback) {
  fetchData(function (err, data) {
    if (err) {
      callback(err);
    } else {
      processData(data, function (err, result) {
        if (err) {
          callback(err);
        } else {
          callback(null, result);
        }
      });
    }
  });
}

Асинхронный/ожидание

async function getData() {
  try {
    const data = await fetchData();
    const result = await processData(data);
    return result;
  } catch (err) {
    throw err;
  }
}

Оба они выполняют одну и ту же задачу. Какой из них вы предпочитаете?

Обработка ошибок с помощью Try & Catch

Еще одним преимуществом использования Async/Await является то, что он предоставляет нам простую и понятную структуру для обработки ошибок. Кроме того, с помощью Try & Catch мы можем обрабатывать ошибки в асинхронной операции, как если бы они возникали в синхронном коде.

Если мы рассмотрим наш предыдущий пример в случае ошибки внутри блока Catch,

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

Пример кода запуска всплывающего сообщения об ошибке

async function fetchPostsData() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');
    const data = await response.json();
    return data;
  } catch (err) {
    triggerErrorToastMessage({message: ‘Request failed’ });
    throw err;
  }
}

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

Параллельное и последовательное выполнение

Параллельное выполнение с Promise.all()

Promise.all() — это встроенный метод в JavaScript. Это позволяет нам запускать несколько промисов параллельно и возвращает результаты всех промисов в одном ответе. MDN — Promise.all()

async function fetchAndProcessData() {
  const [data1, data2, data3] = await Promise.all([
    fetch('https://jsonplaceholder.typicode.com/posts'),
    fetch('https://jsonplaceholder.typicode.com/users'),
    fetch('https://jsonplaceholder.typicode.com/comments)
  ]);

  // Process the fetched data
  const processedData = await processFetchedData(data1, data2, data3);

  return processedData;
}

В этом примере запросы обрабатываются параллельно с помощью Promise.all(), а полученные данные последовательно обрабатываются с помощью async/await.

Последовательное выполнение с Async/Await

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

Давайте создадим пример сценария:

Для транзакции мы попросим зарегистрированного пользователя ввести одноразовый пароль (OTP).

  1. Проверить, является ли пользователь зарегистрированным пользователем в системе,
  2. Проверьте, действителен ли адрес электронной почты пользователя на основе результата первого шага,
  3. Отправьте OTP на адрес электронной почты пользователя на основе результата второго шага,
  4. Вернуть результат третьего шага.

Затем, если процесс прошел успешно, активируйте функцию, которая отображает экран входа в систему OTP.

Пример синтаксиса

async function performTasksSequentially() {
// First task
  const task1Result = await checIsUserRegisteredUser(); 

// Second task after completing first task
  const task2Result = await checkIsMailAddressValid(task1Result); 


  // Third task after completing second task 
  const task3Result = await sendOTPToValidMail(task2Result);

  // Return the final result
  return task3Result;
}

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

Поскольку мы поговорили о принципах синхронной и асинхронной работы, теперь мы можем взглянуть на то, как этот процесс работает в Цикле событий.

Цикл событий

Прежде всего, позвольте мне уточнить, что связь между циклом событий и событиями в HTML такая же, как связь между JavaScript и Java :) Так что никакой связи нет.

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

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

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

Сравнение цикла событий с синхронным и асинхронным кодом

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

Синхронный

function foo(){
  console.log("foo");
}

function bar(){
  console.log("bar");
}

function hello(){
  foo();
  bar();
  console.log ("hello");
}

hello();

Асинхронный

console.log("hello");

function world(){
     console.log("world")
}

setTimeout(function cbFunction(){
     console.log("Callback Function")
}, 1000);

world();

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

Не хочу продолжать это дальше, так как Event Loop — это тема, которую можно рассмотреть отдельно, поэтому я закончу свою статью здесь.

Спасибо, что нашли время, чтобы прочитать его! Ваши мысли и комментарии бесценны. До новых встреч, Arrivederci!

Дополнительная часть

Вот мое любимое видео о Event Loop на Youtube: https://youtu.be/MJeofIcEWLo

Вы можете связаться со мной по указанным ниже каналам,

LinkedIn: https://www.linkedin.com/in/developeroguzyuksel/

GitHub: https://github.com/ooguzyuksel

Огужан ЮКСЕЛЬ

Разработчик внешнего интерфейса ÇSTech

Ссылки и источники: