При стандартном выполнении JavaScript инструкции выполняются последовательно, одна за другой. То есть сначала выполняется первая инструкция, потом вторая и так далее. Однако что, если одна из этих операций выполняется продолжительное время. Например, она выполняет какую-то высоконагруженную работу, как обращение по сети или обращение к базе данных, что может занять неопределенное и иногда продолжительное время. В итоге при последовательном выполнении все последующие операции будут ожидать выполнения этой операции. Чтобы избежать подобной ситуации, JavaScript позволяет избежать подобного сценария с помощью асинхронных функций.
Например, определим простую асинхронную функцию, которая эмулирует долгую работу с помощью вызова setTimeout() и задержки в 1 секунду, а затем выводит на консоль случайное число:
function asyncFunction() { setTimeout(()=>{ let result = 22; console.log("result:", result); }, 1000); } asyncFunction(); console.log("Конец программы");
Вместо setTimeout()
здесь мог бы быть запрос к базе данных или запрос к сетевому ресурсу, которые могли бы занять продолжительное время и результат которых был бы получен через некоторое время.
И в результате значение числа было бы ведено на консоль в самом конце выполнения программы:
Конец программы result: 22
Здесь мы видим, что асинхронная функция не блокирует выполнение остальных инструкций программы. Однако при работе с подобными функциями мы можем столкнуться с рядом проблем. Так, асинхронные функции не возвращают результат асинхронного вычисления через ключевое слово return, а передают его в качестве параметра функции обратного вызова.
function asyncFunction() { let result; setTimeout(()=>{result = 22;}, 1000); return result; } const asyncResult = asyncFunction(); console.log("result:", asyncResult) // result: undefined
Здесь асинхронная функция asyncFunction вызывается в синхронной манере, в итоге мы получаем неопределенный результат. Потому что переменная asyncResult устанавливается до того, как функция asyncFunction сгенерирует результат.
Другая проблема связана с генерацией ошибок через оператор throw:
function asyncFunction() { let result; setTimeout(()=>{ result = 22; if(result < 50) { throw new Error("Некорректное значение"); } }, 1000); return result; } try { const asyncResult = asyncFunction(); console.log("result:", asyncResult) } catch(error) { console.error("Error:", error); // Эта строка НЕ выполняется } console.log("Конец программы");
Здесь обработка ошибки в блоке catch
работать не будет, так как к моменту выдачи ошибки вызывающий код уже ушел и некому поймать ошибку.
Изначально обработки результата и ошибок в асинхронных функциях представляло использование коллбеков-функций обратного вызова, которые передавались в другую функцию и вызывались позже в некоторый момент времени. Простейший шаблон использования коллбеков:
function asyncFunction(callback) { console.log("Перед вызовом коллбека"); callback(); console.log("После вызова коллбека"); } function callbackFunc() { console.log("Вызов коллбека"); } asyncFunction(callbackFunc);
Здесь функция asyncFunction (условно асинхронная функция) принимает функцию обратного вызова - callback и вызывает ее в коде.
Например, используем коллбек для получения и обработки результата и ошибки асинхронной функции:
function handleResult(error, result){ if(error) { // если передана ошибка console.error(error); } else { // если асинхронная функция завершилась успешно console.log("Result:", result); } } function asyncFunction(callback) { setTimeout(()=>{ let result = Math.floor(Math.random() * 100) + 1; if(result < 50) { // если меньше 50, устанавливаем ошибку callback(new Error("Некорректное значение: " + result), null); } else{ // в остальных случаях устанавливаем результат callback(null, result); } }, 1000); } asyncFunction(handleResult);
В качестве коллбека в асинхронную функцию asyncFunction передается функция handleResult
asyncFunction(handleResult);
Для примера, чтобы число представляло случайное значение, здесь применяется метод Math.random()
.
let result = Math.floor(Math.random() * 100) + 1;
Если сгенерированное число меньше 50, то устанавливаем первый параметр функции handleResult, который представляет ошибку:
if(result < 50) { // если меньше 50, устанавливаем ошибку callback(new Error("Некорректное значение: " + result), null); }
В остальных случаях устанавливаем результат, а для ошибки передаем null:
else{ // в остальных случаях устанавливаем результат callback(null, result); }
консольный вывод при успешной обработке (когда сгенерированное число равно или больше 50):
Result: 70
Если сгенерированное число меньше 50, то будет выводиться ошибка:
Error: Некорректное значение: 35
Это классическая схема использования коллбеков для обработки результата асинхронной функции. Однако она имеет как минимум один большой недостаток: чрезмерное использование функций обратного вызова может привести к созданию структуры кода, известной среди разработчиков JavaScript как callback hell (ад коллбеков). Такая структура кода возникает, когда коллбек в одной асинхронной функции вызывает другую асинхронную функцию, коллбек которой, в свою очередь, может вызывать третью асинхронную функцию и так далее. Пример подобной структуры:
asyncFunction( (error, result) => { asyncFunction2( (error2, result2) => { asyncFunction3( (error3, result3) => { asyncFunction4( (error4, result4) => { // некоторый код }); }); }); });
И для решения этой проблемы начиная со стандарта ES2015 в JavaScript была добавлена поддержка промисов, которые далее будут рассмотрены.