Руководство для начинающих по асинхронному JavaScript

javascript
обещания
AsyncAwait
Руководство для начинающих по асинхронному JavaScript cover image

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

1

На диаграмме показан типичный поток выполнения приложения для чтения книг. Если пользователь инициирует длительную операцию (в данном случае поиск всех вхождений буквы «the» в большой книге), приложение «зависает» на все время выполнения этой операции. В этом случае пользователь будет безрезультатно нажимать кнопку следующей закладки до тех пор, пока операция поиска не завершится, и все операции не вступят в силу одновременно, создавая у конечного пользователя ощущение отстающего приложения.

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

2

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

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

Ниже приведено краткое руководство по асинхронному Javascript:

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

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

// Search occurrences function
function searchOccurrences(word, callback) {
  try {
    // search operation logic, result is in result variable
    //....
    callback(null, word, result);
  } catch (err) {
    callback(err);
  }
}

// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
  if (err) {
    console.log(`Search operation for ${word} ended with an error`);
  } else console.log(`Search results for ${word}: ${result}`);
  return;
}

searchOccurrences("the", handleSearchOccurrencesResult);

Сначала мы определяем функцию операции поиска searchOccurrences. Он принимает слово для поиска и второй параметр «обратный вызов», который будет функцией, которая будет выполняться после завершения операции поиска. Функция операции поиска была намеренно оставлена ​​абстрактной, нам нужно сосредоточиться только на двух ее возможных результатах: в первом случае все прошло успешно и у нас есть результат поиска в переменной result. В этом случае нам просто нужно вызвать функцию обратного вызова со следующими параметрами: первый параметр имеет значение null, что означает, что ошибки не произошло, второй параметр — это искомое слово, а третий и, возможно, самый важный параметр из трех, является результатом операции поиска.

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

Затем мы определили функцию обратного вызова handleSearchOccurrences, сохранив ее логику достаточно простой. Это просто вопрос вывода сообщения на консоль. Сначала мы проверяем параметр «err», чтобы увидеть, произошла ли какая-либо ошибка в основной функции. В этом случае мы просто сообщаем пользователю, что операция поиска завершилась с ошибкой. Если ошибок не возникло, мы печатаем сообщение с результатом операции поиска.

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

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

let result;
function searchOccurrences(word, callback) {
  try {
    // search operation logic, result is in searchResult variable
    //....
    result = searchResult;
    callback(null, word, result);
  } catch (err) {
    callback(err);
  }
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);

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

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

functionA(function (err, resA) {
  ///......
  functionB(resA, function (err, resB) {
    ///......
    functionC(resB, function (err, resC) {
      ///......
      functionD(resC, function (err, resD) {
        ///......
      });
    });
  });
});

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

Обещания

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

// Search occurrences function
function searchOccurrences(word) {
  return new Promise((resolve, reject) => {
    try {
      // search operation logic, result is in result variable
      //....
      resolve(word, result);
    } catch (err) {
      reject(err);
    }
  });
}

searchOccurrences("the")
  .then((word, result) => {
    console.log(`Search results for ${word}: ${result}`);
  })
  .catch((err) => {
    console.log(`Search operation ended with an error`);
  });

Давайте пробежимся по внесенным нами изменениям:

Функция searchOccurrences возвращает обещание. Внутри промиса мы сохраняем ту же логику: у нас есть две функцииsolve и ignore, которые представляют наши обратные вызовы, а не одна функция обратного вызова, которая обрабатывает как успешное выполнение, так и выполнение с ошибками. Промисы разделяют два результата и обеспечивают понятный синтаксис при вызове основной функции. Функция разрешения «подключается» к основной функции с помощью ключевого слова «then». Здесь мы просто указываем два параметра функции разрешения и печатаем результат поиска. То же самое относится и к функции отклонения, ее можно перехватить с помощью ключевого слова «catch». Надеемся, вы сможете оценить преимущества обещаний с точки зрения читаемости и чистоты кода. Если вы все еще спорите об этом, узнайте, как мы можем решить проблему ада обратных вызовов, объединив асинхронные функции, которые будут выполняться одна за другой:

searchOccurrences("the")
  .then(searchOccurrences("asynchronous"))
  .then(searchOccurrences("javascript"))
  .then(searchOccurrences("guide"))
  .catch((err) => {
    console.log(`Search operation ended with an error`);
  });

Асинхронизация/ожидание

Async/Await — последнее дополнение к нашему набору асинхронных инструментов в Javascript. Представленные в ES8, они обеспечивают новый уровень абстракции поверх асинхронных функций, просто «ожидая» выполнения асинхронной операции. Выполнение программы блокируется на этой инструкции до тех пор, пока не будет возвращен результат асинхронной операции, а затем программа перейдет к следующей инструкции. Если вы думаете о синхронном потоке выполнения, вы правы. Мы прошли полный круг! Async/await пытается привнести простоту синхронного программирования в асинхронный мир. Имейте в виду, что это проявляется только в исполнении и коде программы. Под капотом все остается прежним: Async/await по-прежнему используют промисы, а обратные вызовы являются их строительными блоками.

Давайте рассмотрим наш пример и реализуем его с помощью Async/await:

async function searchOccurrences(word) {
  try {
    // search operation logic, result is in result variable
    //....
    return result;
  } catch (err) {
    console.log(`Search operation for ${word} ended with an error`);
  }
}

const word = "the";

const result = await searchOccurrences(word, handleSearchOccurrencesResult);

console.log(`Search results for ${word}: ${result}`);

Наш код не сильно изменился, здесь важно отметить ключевое слово «async» перед объявлением функции searchOccurrences. Это указывает на то, что функция является асинхронной. Также обратите внимание на ключевое слово «await» при вызове функции searchOccurrences. Это даст указание программе дождаться выполнения функции до тех пор, пока не будет возвращен результат, прежде чем программа сможет перейти к следующей инструкции, другими словами, переменная результата всегда будет содержать возвращаемое значение функции searchOccurrences, а не обещание в этом смысле функция Async/Await не имеет состояния ожидания, как Promises. После завершения выполнения мы переходим к оператору печати, и на этот раз результат фактически содержит результат операции поиска. Как и ожидалось, новый код ведет себя так же, как если бы он был синхронным.

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

Заворачивать

В этой статье мы рассмотрели различные подходы, используемые для реализации асинхронной логики в Javascript. Мы начали с рассмотрения конкретного примера того, почему нам необходимо перейти от обычного синхронного стиля программирования к асинхронной модели. Затем мы перешли к обратным вызовам, которые являются основными строительными блоками асинхронного Javascript. Ограничения обратных вызовов привели нас к различным альтернативам, которые добавлялись с течением времени для преодоления этих ограничений, в основном это обещания и Async/await. Асинхронную логику можно найти где угодно в Интернете, независимо от того, вызываете ли вы внешний API, инициируете запрос к базе данных, записываете в локальную файловую систему или даже ожидаете ввода пользователя в форме входа. Надеемся, теперь вы чувствуете себя более уверенно в решении этих проблем, написав чистый и удобный в сопровождении асинхронный Javascript!

Если вам понравилась эта статья, посетите блог CLA, где мы обсуждаем различные темы о том, как заняться технологиями. Также посетите наш канал YouTube, чтобы узнать о наших предыдущих бесплатных семинарах, и подписывайтесь на нас в социальных сетях. -labs-academy/), чтобы не пропустить предстоящие!


Career Services background pattern

Карьерные услуги

Contact Section background image

Давай останемся на связи

Code Labs Academy © 2024 Все права защищены.