Callbacks

Callback — функция, переданная как аргумент в другую функцию, которая будет вызвана позже (после завершения операции).

Зачем нужно

Callback — исторически первый и самый простой способ работы с асинхронным кодом. Многие API (таймеры, события, Node.js) до сих пор используют callbacks.

Где используется

  • setTimeout / setInterval
  • addEventListener
  • Node.js API (fs.readFile, http.get)
  • Методы массивов (map, filter, forEach)
  • Обработка событий

Предпосылки

Event Loop, Scope

Синхронные 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);
  }
};

Практика

  1. Напиши функцию loadData(url, callback) с error-first паттерном
  2. Создай «callback hell» из 3 вложенных операций и перепиши в Promise
  3. Напиши свою функцию promisify
  4. Реализуй 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'а откладывается во времени и приходит из внешнего источника событий.»


🎓 Источник: 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-контракта.