Асинхронное программирование в JavaScript и коллбэки
Асинхронность в языках программирования
Компьютеры асинхронны по своей конструкции.
Асинхронность означает, что события могут происходить независимо от основного потока программы.
В современных потребительских компьютерах каждая программа выполняется в течение определенного временного интервала, а затем останавливает свое выполнение, чтобы позволить другой программе продолжить свое выполнение. Это происходит в цикле настолько быстро, что это невозможно заметить. Мы думаем, что наши компьютеры запускают множество программ одновременно, но это иллюзия (за исключением многопроцессорных машин).
Программы внутренне используют прерывания, сигнал, который выдается процессору, чтобы привлечь внимание системы.
Не будем вдаваться в подробности сейчас, просто имейте в виду, что для программ нормально быть асинхронными и приостанавливать свое выполнение до тех пор, пока им не потребуется внимание, позволяя компьютеру выполнять другие вещи в это время. Когда программа ждет ответа от сети, она не может остановить процессор до завершения запроса.
Обычно языки программирования синхронны, и некоторые предоставляют способ управления асинхронностью в языке или через библиотеки. C, Java, C#, PHP, Go, Ruby, Swift и Python - все синхронны по умолчанию. Некоторые из них обрабатывают асинхронные операции с помощью потоков, порождая новый процесс.
JavaScript
JavaScript по умолчанию синхронный и однопоточный. Это означает, что код не может создавать новые потоки и запускать их параллельно.
Строки кода выполняются последовательно, одна за другой, например:
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();
Но JavaScript родился внутри браузера, его основной задачей, в начале, было реагировать на действия пользователя, такие как onClick
, onMouseOver
, onChange
, onSubmit
и так далее. Как он мог это сделать с синхронной моделью программирования?
Ответ был в его окружении. Браузер предоставляет способ сделать это, предоставляя набор API, которые могут обрабатывать этот тип функциональности.
Совсем недавно Node.js представил неблокирующую среду ввода-вывода, чтобы расширить эту концепцию до доступа к файлам, сетевых вызовов и так далее.
Колбэки
Вы не можете знать, когда пользователь нажмет на кнопку. Поэтому вы определяете обработчик событий для события click. Этот обработчик событий принимает функцию, которая будет вызвана при возникновении события:
document.getElementById('button').addEventListener('click', () => {
// элемент был нажат
});
Это так называемый колбэк (callback).
Колбэк - это простая функция, которая передается в качестве значения другой функции и будет выполнена только при наступлении события. Мы можем это сделать, потому что JavaScript имеет функции первого класса, которые могут быть присвоены переменным и переданы другим функциям (называемым функциями высшего порядка)
Распространено оборачивать весь клиентский код в слушатель события load на объекте window, который запускает функцию обратного вызова только тогда, когда страница готова:
window.addEventListener('load', () => {
// окно загружено
// делайте что хотите
});
Колбэки используются повсюду, а не только в событиях DOM.
Один из распространенных примеров - использование таймеров:
setTimeout(() => {
// выполняется через 2 секунды
}, 2000);
XHR-запросы также принимают колбэк, в этом примере путем присвоения функции свойству, которое будет вызываться при возникновении определенного события (в данном случае, изменение состояния запроса):
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error');
}
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();
Обработка ошибок в колбэках
Как обрабатывать ошибки с помощью колбэков? Одна из самых распространенных стратегий - использовать то, что принято в Node.js: первым параметром в любой функции обратного вызова является объект ошибки: колбэки, ориентированные на ошибки (error-first callbacks)
Если ошибки нет, объект имеет значение null. Если есть ошибка, он содержит некоторое описание ошибки и другую информацию.
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
if (err) {
// обработать ошибку
console.log(err);
return;
}
// ошибок нет, обработать данные
console.log(data);
});
Проблема с колбэками
Коллбэки отлично подходят для простых случаев!
Однако каждый коллбэк добавляет уровень вложенности, и когда у вас много коллбэков, код начинает очень быстро усложняться:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// ваш код здесь
});
}, 2000);
});
});
Это всего лишь простой 4-уровневый код, но я видел гораздо больше уровней вложенности, и это не весело.
Как мы это решаем?
Альтернативы коллбэкам
Начиная с ES6, JavaScript представил несколько функций, которые помогают нам с асинхронным кодом, не требуя использования коллбэков: Promises
(ES6) и Async/Await
(ES2017).