Синхронный и асинхронный код в JavaScript

Рассмотрим следующий код:

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); }

Изучать способы обработки исключений в асинхронном коде мы будем в следующих уроках.