Рассмотрим следующий код:
console.log('1');
console.log('2');
Очевидно, что сначала сработает первый вывод в консоль, а потом - второй. То есть команды нашего кода выполняются по очереди - в порядке их следования в коде. Такой код называется синхронным.
Рассмотрим теперь следующий код:
setTimeout(function() {
console.log('1');
}, 3000);
console.log('2');
В данном случае команды не выполняются в порядке следования в коде: первый вывод в консоль выполнится, когда приходит его время, но остальной код не ожидает этого момента, а продолжает выполнятся. Такой код называется асинхронным.
Асинхронный код возникает в JavaScript достаточно часто, просто вы пока сталкивались только с одним очевидным его проявлением: с таймерами. Менее очевидно то, что событийная модель JavaScript - тоже асинхронна.
Событийная модель асинхронна
Вы знаете, что с помощью метода addEventListener
можно подписаться на различные события,
возникающие в элементах страницы. К примеру,
подпишемся на клик по некоторому элементу,
ссылка на который хранится в переменной elem
:
elem.addEventListener('click', function() {
console.log('1');
});
console.log('2');
В приведенном коде сначала выполнится второй вывод в консоль. Когда же выполнится первый? Когда-то: выполнение этого кода ожидает случившегося события - клика на элемент. Как только это произойдет, так наш код и выполнится.
Загрузка картинок асинхронна
Рассмотрим следующий код:
let image = document.createElement('img');
image.src = 'img.png';
document.body.appendChild(image);
Как вы видите, здесь мы создаем тег img
,
записываем в его src
путь к картинке
и размещаем эту картинку в body
. Картинка,
однако, появится на странице не сразу. Дело
в том, что когда мы записали в src
путь к картинке - браузер начинает скачивать
эту картинку с сайта. Как только картинка
будет скачана, только тогда браузер сможет
ее показать.
Если картинка достаточно большая, а скорость интернета достаточно маленькая, то пользователь сайта некоторое время сможет "полюбоваться" на пустую картинку - пока она не подгрузится.
На самом деле у тега img
существует
событие load
, которое срабатывает
при окончании загрузки картинки:
let image = document.createElement('img');
image.src = 'img.png';
image.addEventListener('load', function() {
// сработает по загрузке картинки
});
Мы можем использовать это, чтобы разместить картинку на странице, только когда эта картинка будет загружена:
let image = document.createElement('img');
image.src = 'img.png';
image.addEventListener('load', function() {
document.body.appendChild(image); // размещаем по загрузке
});
Картинка не обязательно загрузится: может
быть такое, что путь к картинке неверный,
либо произойдет обрыв интернета, поломка
сервера с сайтом или что-то подобное. Говоря
другими словами - исключительная ситуация.
В этом случае сработает не событие load
,
а событие error
:
let image = document.createElement('img');
image.src = 'img.png';
image.addEventListener('load', function() {
document.body.appendChild(image);
});
image.addEventListener('error', function() {
// ошибка загрузки картинки
});
Нам нужно будет как-то эту исключительную ситуацию обработать: выдать сообщение об ошибке, попробовать еще раз загрузить картинку через некоторое время, либо сделать что-то еще - это вы решаете сами, как разработчик.
Где еще будет асинхронность?
Итак, таймеры, события, загрузка картинок - это те места, где вы пока могли столкнутся с асинхронным поведением кода. Где же еще вас ждет асинхронность?
Во-первых, и самое важное - это AJAX запросы, позволяющие обращаться к серверу без перезагрузки страницы. Во-вторых, это NodeJS, в котором практически весь код - асинхронный.
AJAX и NodeJS - это очень важно. Поэтому перед их изучением вам необходимо разобраться с асинхронностью. Делать мы это будем на простых примерах с таймерами, которые в нашем случае будут имитировать асинхронный код, с которым вам придется столкнутся при изучении более сложных вещей.
С помощью таких имитаторов вы сможете понять поведение асинхронного кода, чтобы затем применить полученные знания при изучении NodeJS, AJAX, и других вещей, где требуется асинхронность.
Наши заглушки-имитаторы
Пусть у нас есть некоторая функция make
,
внутри которой выполняется асинхронный код:
function make() {
// асинхронный код
}
Это может быть все, что угодно: таймер, загрузка картинки, AJAX запрос к серверу, чтение или запись файла через NodeJS и так далее.
Понятно, что из всего перечисленного вы умеете работать только с первыми двумя пунктами. Поэтому в качестве асинхронного кода мы используем таймер, имитирующий какой угодно асинхронный код:
function make() {
setTimeout(function() {
console.log('1');
}, 3000);
}
Давайте теперь воспользуемся нашей функцией в коде:
make();
console.log('2');
Посмотрите внимательно на этот код: по его
внешнему виду, не глядя в код функции make
теперь невозможно определить синхронный он
или асинхронный! Все зависит от того, что
же там внутри нашей функции. Однако, запуск
кода сразу проявит картину: сначала выполнится
второй вывод в консоль, и только потом - первый.
Основная задача в асинхронности
Посмотрим еще раз на наш код:
make();
console.log('2');
В нашем случае код, написанный после вызова функции make, выполнится после срабатывания асинхронного кода внутри функции.
Иногда это то, что нам нужно, но чаще всего - нет. Чаще всего мы хотим сделать так, чтобы некоторый код выполнился после асинхронной операции.
Например: мы хотим показать картинку на экране после ее загрузки, мы хотим что-то сделать с текстом прочитанного файла после его загрузки, мы хотим что-то сделать с результатом AJAX запроса после его выполнения и тп.
Можно утверждать, что это является основной возникающей задачей при работе с асинхронностью. Для ее решения могут быть использованы подходы, которые мы будем изучать в следующих уроках: коллбэки, подписки и промисы.
Исключительные ситуации
Второй частой задачей при работе с асинхронным
кодом является обработка исключительных ситуаций.
Дело в том, что исключение, возникшее внутри
асинхронного кода, не может быть поймано
через try-catch
:
try {
setTimeout(function() {
throw(new Error); // исключение не будет поймано
}, 3000);
} catch(error) {
console.log(error);
}
Из-за такого поведения мы легко можем попасть в неожиданную ловушку, например, вот так:
try {
elem.addEventListener('click', function() {
throw(new Error); // исключение не будет поймано
});
} catch(error) {
console.log(error);
}
Изучать способы обработки исключений в асинхронном коде мы будем в следующих уроках.