Компьютеры по своей природе асинхронны.
Асинхронность означает, что вещи могут происходить независимо от основного потока программы.
В современных потребительских компьютерах каждая программа работает в течение отведённого ей промежутка времени, а затем останавливает выполнение, чтобы дать другой программе продолжить своё. Всё это крутится в цикле настолько быстро, что заметить это невозможно. Нам кажется, что наши компьютеры выполняют много программ одновременно, но это иллюзия (кроме многопроцессорных машин).
Внутри программы используют прерывания (interrupts) — сигнал, который посылается процессору, чтобы привлечь внимание системы.
Не будем сейчас углубляться в детали, просто держите в голове, что для программ нормально быть асинхронными и приостанавливать выполнение, пока им не потребуется внимание, позволяя компьютеру тем временем выполнять другие вещи. Когда программа ждёт ответа из сети, она не может застопорить процессор, пока запрос не завершится.
Обычно языки программирования синхронны, и некоторые предоставляют способ управлять асинхронностью — в самом языке или через библиотеки. C, Java, C#, PHP, Go, Ruby, Swift и Python по умолчанию синхронны. Некоторые из них справляются с асинхронными операциями с помощью потоков, порождая новый процесс.
JavaScript по умолчанию синхронен и однопоточен. Это значит, что код не может создавать новые потоки и работать параллельно.
Строки кода выполняются последовательно, одна за другой, например:
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();Но JavaScript родился внутри браузера, и его главной задачей поначалу было реагировать на действия пользователя — onClick, onMouseOver, onChange, onSubmit и так далее. Как он мог делать это при синхронной модели программирования?
Ответ был в его окружении. Браузер даёт способ это делать, предоставляя набор API, которые справляются с такого рода функциональностью.
Позже Node.js внедрил неблокирующее окружение ввода-вывода (I/O), чтобы распространить эту концепцию на доступ к файлам, сетевые вызовы и так далее.
Вы не можете знать, когда пользователь нажмёт кнопку. Поэтому вы определяете обработчик события click. Этот обработчик принимает функцию, которая будет вызвана, когда событие сработает:
document.getElementById('button').addEventListener('click', () => {
// элемент нажат
});Это и есть так называемый колбэк (callback).
Колбэк — это обычная функция, которая передаётся как значение другой функции и выполняется только тогда, когда произойдёт событие. Мы можем так делать, потому что в JavaScript функции — «граждане первого класса» (first-class functions): их можно присваивать переменным и передавать в другие функции (называемые функциями высшего порядка, higher-order functions).
Часто весь клиентский код оборачивают в слушатель события load на объекте window, который запускает функцию-колбэк только когда страница готова:
window.addEventListener('load', () => {
// окно загружено
// делайте что хотите
});Колбэки используются повсюду, не только в DOM-событиях.
Один распространённый пример — таймеры:
setTimeout(() => {
// выполнится через 2 секунды
}, 2000);XHR-запросы тоже принимают колбэк — в этом примере через присваивание функции свойству, которое будет вызвано при возникновении определённого события (в данном случае — при изменении состояния запроса):
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error('error');
}
}
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();Как обрабатывать ошибки при работе с колбэками? Одна очень распространённая стратегия — та, что переняли в Node.js: первым параметром любой функции-колбэка является объект ошибки. Это error-first колбэки (колбэки с ошибкой на первом месте).
Если ошибки нет, объект равен 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).