Вспомним код, сделанный нами в предыдущем уроке:
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.
Но, расстраиваться не стоит - решение проблемы возможно через промисы, которые мы будем изучать в следующих уроках.