Callbacks
Callback — функция, переданная как аргумент в другую функцию, которая будет вызвана позже (после завершения операции).
Зачем нужно
Callback — исторически первый и самый простой способ работы с асинхронным кодом. Многие API (таймеры, события, Node.js) до сих пор используют callbacks.
Где используется
setTimeout/setIntervaladdEventListener- Node.js API (fs.readFile, http.get)
- Методы массивов (map, filter, forEach)
- Обработка событий
Предпосылки
Синхронные callbacks
// Callbacks в методах массивов — синхронные
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(num) {
console.log(num); // Выполняется немедленно
});
const doubled = numbers.map(num => num * 2);
const evens = numbers.filter(num => num % 2 === 0);
// Кастомная функция с callback
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log); // 0, 1, 2
Асинхронные callbacks
// setTimeout — callback вызывается позже
console.log('до');
setTimeout(function {
console.log('внутри таймера'); // через ~1 секунду
}, 1000);
console.log('после');
// Вывод: до, после, внутри таймера
// addEventListener — callback при событии
const button = document.querySelector('#btn');
button.addEventListener('click', function(event) {
console.log('Кнопка нажата!', event.target);
});
// Загрузка изображения
function loadImage(src, onSuccess, onError) {
const img = new Image();
img.onload = () => onSuccess(img);
img.onerror = () => onError(new Error(`Не удалось загрузить ${src}`));
img.src = src;
}
loadImage(
'photo.jpg',
img => document.body.append(img),
err => console.error(err)
);
Error-first callbacks (Node.js стиль)
// Конвенция: первый аргумент callback — ошибка
// Если ошибки нет — null
// Пример: чтение файла в Node.js
const fs = require('fs');
fs.readFile('data.txt', 'utf8', function(err, data) {
if (err) {
console.error('Ошибка чтения:', err.message);
return;
}
console.log('Данные:', data);
});
// Свой error-first callback
function divide(a, b, callback) {
if (b === 0) {
callback(new Error('Деление на ноль'));
return;
}
callback(null, a / b);
}
divide(10, 2, (err, result) => {
if (err) {
console.error(err.message);
return;
}
console.log(`Результат: ${result}`); // Результат: 5
});
divide(10, 0, (err, result) => {
if (err) {
console.error(err.message); // Деление на ноль
return;
}
console.log(result);
});
Callback Hell (Ад обратных вызовов)
// Когда одна асинхронная операция зависит от другой:
getUser(userId, function(err, user) {
if (err) { handleError(err); return; }
getOrders(user.id, function(err, orders) {
if (err) { handleError(err); return; }
getOrderDetails(orders[0].id, function(err, details) {
if (err) { handleError(err); return; }
getShippingInfo(details.trackingId, function(err, shipping) {
if (err) { handleError(err); return; }
updateUI(user, orders, details, shipping);
// «Пирамида смерти» — код уходит вправо
});
});
});
});
Решение 1: Именованные функции
function handleShipping(user, orders, details) {
return function(err, shipping) {
if (err) { handleError(err); return; }
updateUI(user, orders, details, shipping);
};
}
function handleDetails(user, orders) {
return function(err, details) {
if (err) { handleError(err); return; }
getShippingInfo(details.trackingId, handleShipping(user, orders, details));
};
}
function handleOrders(user) {
return function(err, orders) {
if (err) { handleError(err); return; }
getOrderDetails(orders[0].id, handleDetails(user, orders));
};
}
function handleUser(err, user) {
if (err) { handleError(err); return; }
getOrders(user.id, handleOrders(user));
}
getUser(userId, handleUser);
Решение 2: Promise (лучше)
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => getShippingInfo(details.trackingId))
.then(shipping => updateUI(shipping))
.catch(handleError);
Решение 3: async/await (лучше всего)
async function loadUserData(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const shipping = await getShippingInfo(details.trackingId);
updateUI(user, orders, details, shipping);
} catch (err) {
handleError(err);
}
}
Промисификация (Callback -> Promise)
// Превращаем callback-функцию в Promise
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
// Использование
const readFile = promisify(fs.readFile);
const data = await readFile('data.txt', 'utf8');
// Node.js имеет встроенный util.promisify
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
Частые ошибки
1. Забыл return после ошибки
function process(data, callback) {
if (!data) {
callback(new Error('Нет данных'));
// Забыл return! Код продолжит выполнение
}
// Этот код выполнится даже при ошибке
callback(null, data.toString());
}
2. Вызов callback несколько раз
function bad(callback) {
callback(null, 'первый вызов');
callback(null, 'второй вызов'); // Баг!
}
3. Потеря контекста this
const obj = {
name: 'Алиса',
greet {
setTimeout(function {
// console.log(this.name); // undefined — this потерян
}, 100);
// Решение: arrow function
setTimeout(() => {
console.log(this.name); // 'Алиса'
}, 100);
}
};
Практика
- Напиши функцию
loadData(url, callback)с error-first паттерном - Создай «callback hell» из 3 вложенных операций и перепиши в Promise
- Напиши свою функцию
promisify - Реализуй
retry(fn, attempts, callback)— повтор при ошибке
Связанные темы
Ресурсы
🎓 Источник: Асинхронное программирование на callback'ах в JavaScript
- 📅 2018-11-13 · YouTube
- Тезисы:
- Callback ≠ асинхронность.
arr.map(fn)тоже callback, и он синхронный. Асинхронность приходит ИЗВНЕ — от host API (таймеры, I/O, события). - Контракт callback-last + error-first — стандарт Node.js. Без соглашения код перемешивается.
- Без явного контроля порядка callback-вызовы дают случайный порядок выполнения — нужен счётчик завершений (
callbackCheck) или последовательная цепочка. - Последовательная вложенность callback'ов = "лесенка" (callback hell) — control flow размазан по коду.
chain(fn1)(arg1).chain(fn2)(arg2)...— функциональная композиция асинхронных функций (двусвязный список через замыкание, обходится в обе стороны).- Передавать
nullвместоundefinedдля отсутствия ошибки — стабильная форма объекта в V8 лучше оптимизируется. - Именованные функции в коллбэках помогают читать стек-трейс, но создают разрыв между логикой и местом вызова.
- Callback ≠ асинхронность.
- Цитата:
«Callback не значит асинхронность. Асинхронность — это когда вызов вашего callback'а откладывается во времени и приходит из внешнего источника событий.»
🎓 Источник: Asynchronous Programming in Node.js and JavaScript
- 📅 2018-09-19 · YouTube
- Тезисы:
- В Node.js без асинхронности никак — это базовая модель работы с I/O.
- Всё сводится к коллбэкам; Promise, async/await — лишь надстройки, абстракции.
- Callback-last error-first — нужно соблюдать неукоснительно; без соглашения весь код становится несовместимым.
- "Беда callback-стиля: функция знает, какая будет следующая в цепочке" — это нарушение SRP.
- Свой EventEmitter мешает параллельности, если использовать его как замену Promise (один на все слушатели).
- Адаптеры toAsync / promisify / callbackify нужны для смешения стилей.
- Утилиты once, limit, throttle, debounce — поверх callback-контракта.