При стандартном выполнении 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 была добавлена поддержка промисов, которые далее будут рассмотрены.