Асинхронность, promise, async и await

Асинхронные функции и коллбеки

Последнее обновление: 20.11.2023

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

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850