Artem Gorev

Мой дорогой дневничок


JavaScript синхронный язык программирования и однопоточный. Это означает, что наш код не может создавать новые потоки и выполняться параллельно. Давайте разберемся, что значит асинхронный код и как он выглядит в JavaScript.

Компьютеры всегда были асинхронными. Асинхронный значит, то что разные процессы могут протекать внезависимости от основного потока программы. В персональных компьютерах, любая программа работает только в определенный промежуток времени, после этого она прерывает свое выполнение и передает управление другой программе. Эти действия настолько быстро, что невозможно заметить момент переключения. Этот механизм создает впечатление, что компьютеры могут выполнять несколько программа одновременно. Еще существуют многопроцессорные компьютеры, но сегодня мы не будем говорить о них.

Программы используют прерывания, сигналы, которые посылаются процессору для получения внимания операционной системы. Мы не будем погружаться в детали устройства процессоров, операционных систем, запомните следующий момент, чтобы программа была асинхронной, она должна уметь отдавать управление и уметь реагировать на определенные события. Нет смысла ждать когда выполнится сетевой запрос нагружая процессор бесполезной работой. Когда придет время ОС скажет, что появились данные.

Популярные языки такие как C, Java, C#, PHP, Go, Swift, Python синхронные, чтобы добиться асинхронности они применяются потоки или процессы, которые являются либо частью языка либо доступны через библиотеки.

JavaScript

Как мы уже отмечали JavaScript является синхронный языком программирования. Он не может создавать процессы или потоки. Он работает только в одном потоке и работает внутри браузера, еще одно существенное ограничение. Но все мы знаем как устроен браузер, в нем может происходить удивительное количество разнообразных действий, таких как onClick, onMouseOver, onSubmit и т.д. Как это может работать в синхронной модели? Ответ кроется в окружение. Браузер предоставляется набор API, которые могут разрешить подобные задачи.

Существует Node.js, в нем представлена неблокирующее окружение ввода/вывода, которая позволяет получить доступ к файлам, сетевым вызовам и т.д.

Функции обратного вызова Callbacks.

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

document.getElementById('button').addEventListener('click', () => {
  //... item clicked
});

Такие функции еще называют функциями обратного вызова или callback’и. Callback это обычная функция, которая передается в другую функцию как параметр. Когда callback будет вызван решает уже функция в которую его передали. Мы можем совершать такие действия, потому что JavaScript поддерживает функции первого класс. Это означает, что мы можем присваивать переменным функции и передавать их в другие функции, так называемые функции высокого порядка.

Callback’и используются везде не только для обработки событий DOM’a. Например,

setTimeout( () => {
  // Вызов произойдет через 2 секунды
}, 2000);

Для сетевых запросов

const request = new XMLHttpRequest();
request.onload = () => console.log(JSON.parse(this.responseText));
request.onerror = err => console.log('Fetch Error :-S', err);
request.open('get', './api/some.json', true);
request.send();

Обработка ошибок

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

fs.readFile('/first.file.json', (err, data) => {
  if(err) {
    // обрабатываем ошибку
    return;
  }
  
  // ошибок не произошло, действуем по плану
  console.log(data);
});

Callback-hell

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

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
});

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

Начиная с ES6 в JavaScript появились Promise’ы, которые частично помогли справиться с недостатками callback’ов. Но сделать удобным написание асинхронного кода получилось только в ES8 с помощью Async/Await.