Асинхронный код с коллбэками в JavaScript

Вспомним код, сделанный нами в предыдущем уроке:

make(); console.log('2');

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

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

make(function() { console.log('2'); });

Конечно же, само по себе это не решит нашу задачу. Пока мы просто заключили следующее соглашение: при желании выполнить код после срабатывания make передайте этот код коллбэком в вызов make.

Исправим код функции make так, чтобы она начала работать в соответствии с нашим соглашением:

function make(callback) { setTimeout(function() { console.log('1'); // некая асинхронная операция, может не одна callback(); // затем наш коллбэк }, 3000); }

Передача результата в коллбэк

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

function make() { setTimeout(function() { let res = [1, 2, 3, 4, 5]; // массив с результатом }, 3000); }

Сделаем так, чтобы массив с результатом передавался в параметр коллбэка:

function make(callback) { setTimeout(function() { let res = [1, 2, 3, 4, 5]; callback(res); // передаем результат параметром }, 3000); }

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

make(function(res) { console.log(res); // наш массив });

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

Передача параметров в асинхронную функцию

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

make(3, function(res) { console.log(res); // третий элемент массива });

Давайте переделаем код нашей функции make в соответствии с описанным:

function make(num, callback) { setTimeout(function() { let arr = [1, 2, 3, 4, 5]; callback(arr[num]); // результатом передаем элемент массива }, 3000); }

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

Обработка исключений в коллбэках

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

try { make(10, function(res) { console.log(res); }); } catch(err) { // не поймается }

В коллбэк-подходе с исключениями работают следующим образом: в первый параметр коллбэка отправляют результат, а во второй - ошибку. В этом случае обработка ошибок происходит следующим образом:

make(10, function(res, err) { if (!err) { console.log(res); // ошибки не возникло, выведем результат } else { console.log(err); // ошибка возникла, выведем ее текст } });

Давайте переделаем код нашей функции make в соответствии с описанным:

function make(num, callback) { setTimeout(function() { let arr = [1, 2, 3, 4, 5]; let err; if (arr[num] === undefined) { err = 'elem not exists'; // текст ошибки } else { err = null; // ошибки нет } callback(arr[num], err); }, 3000); }

Практическая задача

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

loadImage('img.png', function() { // выполнится по загрузке картинки });

Пусть в первый параметр нашего коллбэка попадает ссылка на DOM элемент картинки, а во второй - ошибка, если произойдет исключительная ситуация:

loadImage('img.png', function(image, err) { console.log(image, err); });

Мы можем использовать нашу функцию следующим образом:

loadImage('image.png', function(image, err) { document.body.append(image); // разместим картинку по загрузке });

Либо с обработкой исключительной ситуации:

loadImage('image.png', function(image, err) { if (!err) { document.body.append(image); } else { console.log('произошла ошибка: ' + err); } });

Реализуйте функцию loadImage. Используйте для этого изученный вами ранее код для загрузки картинок.

Callback hell

Пусть мы хотим с помощью функции loadImage загрузить три картинки:

loadImage('img1.png', function(image, err) { document.body.append(image); }); loadImage('img2.png', function(image, err) { document.body.append(image); }); loadImage('img3.png', function(image, err) { document.body.append(image); });

С этим кодом кое-что не так. Дело в том, что картинки будут добавляться в body по мере их загрузки. То есть никто не гарантирует нам, что картинки будут добавлены именно в том порядке, который нам нужен.

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

Окей, переделаем код:

loadImage('img1.png', function(image1, err1) { document.body.append(image1); loadImage('img2.png', function(image2, err2) { document.body.append(image2); loadImage('img3.png', function(image3, err3) { document.body.append(image3); console.log('все картинки загружены'); }); }); });

Мы решили обе описанные проблемы. Однако, получили взамен другую. Пока она еще не сильно видна, но представьте себе, как будет выглядеть наш код, если в нем будет загрузка не трех, а, скажем, десяти картинок, плюс будет добавлена обработка исключений. В результате код станет крайне нечитаемым: сложность кода лавинообразно нарастает при вложенности коллбэков друга. Такая ситуация называется callback hell - ад коллбэков.

Перепишите приведенный код так, чтобы в нем была загрузка 10 картинок плюс обработка исключений.

Загрузка картинок в цикле

Пусть пути к картинкам хранятся в массиве:

let arr = ['img1.png', 'img2.png', 'img3.png'];

Мы можем загрузить эти картинки в цикле:

for (let path of arr) { loadImage(path, function(image, err) { document.body.append(image); }); }

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

И решения в данной ситуации нет: невозможно запустить цикл, использовать внутри него асинхронную функцию, а потом поймать момент завершения всех функций цикла. Либо вам не нужно ловить этот момент и приведенный выше код вам подойдет либо добро пожаловать в callback hell.

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