Урок 02

Callback-функции

Callback — функция, которая передаётся другой функции и вызывается позже. Самый старый способ работы с асинхронностью в JavaScript.

Простой callback

function greet(name, callback) {
    console.log(`Привет, ${name}`);
    callback();
}

greet('Алиса', () => {
    console.log('Callback вызван');
});

Асинхронный callback

function loadData(callback) {
    setTimeout(() => {
        const data = { id: 1, name: 'Алиса' };
        callback(data);
    }, 1000);
}

loadData((data) => {
    console.log(data);
});

loadData не возвращает данные сразу. Она вызывает callback, когда данные готовы.

Callback с ошибкой

Стандартный паттерн Node.js — error-first callback:

function loadUser(id, callback) {
    setTimeout(() => {
        if (id <= 0) {
            callback(new Error('Неверный ID'), null);
            return;
        }
        callback(null, { id, name: 'Алиса' });
    }, 1000);
}

loadUser(1, (error, user) => {
    if (error) {
        console.error(error);
        return;
    }
    console.log(user);
});

Первым аргументом всегда идёт ошибка (или null), вторым — результат.

Встроенные примеры

setTimeout, addEventListener, методы массивов:

setTimeout(() => console.log('позже'), 1000);
button.addEventListener('click', () => console.log('клик'));
[1, 2, 3].forEach((x) => console.log(x));

Проблема: callback hell

Когда много вложенных callback-ов — код превращается в «пирамиду»:

loadUser(1, (err, user) => {
    if (err) return handleError(err);

    loadPosts(user.id, (err, posts) => {
        if (err) return handleError(err);

        loadComments(posts[0].id, (err, comments) => {
            if (err) return handleError(err);

            loadAuthor(comments[0].authorId, (err, author) => {
                if (err) return handleError(err);

                console.log(author);
            });
        });
    });
});

Проблемы:

  • Тяжело читать
  • Сложно обрабатывать ошибки
  • Легко забыть return
  • Сложно рефакторить

Решение: промисы

loadUser(1)
    .then((user) => loadPosts(user.id))
    .then((posts) => loadComments(posts[0].id))
    .then((comments) => loadAuthor(comments[0].authorId))
    .then((author) => console.log(author))
    .catch(handleError);

Плоско, читаемо, одна точка обработки ошибок.

Когда callbacks всё ещё нужны

  • addEventListener — реакция на события
  • Методы массивов: forEach, map, filter
  • setTimeout
  • Библиотеки, не поддерживающие промисы
  • Node.js API с error-first

Преобразование в промис

function loadUserPromise(id) {
    return new Promise((resolve, reject) => {
        loadUser(id, (err, user) => {
            if (err) reject(err);
            else resolve(user);
        });
    });
}

await loadUserPromise(1);

Такой процесс называется промисификацией.

Итоги

  • Callback — функция, вызываемая позже
  • Error-first — стандарт Node.js
  • Вложенные callbacks — «callback hell»
  • Промисы решают проблему
  • addEventListener и методы массивов всё ещё на callbacks