Callback hell: Эпоха до Промисов

09-06-24 16:23:49


Image for the Callback hell: Эпоха до Промисов

Что такое Callback Hell?

Эта ситуация возникает, когда несколько асинхронных операций вложены друг в друга, создавая глубокую, пирамидальную структуру коллбеков. Такое вложение делает код трудным для чтения, отладки и поддержки.

Пример

Рассмотрим сценарий, в котором вам нужно выполнить серию асинхронных операций: получить данные пользователя, затем посты этого пользователя, а затем комментарии к каждому посту. Используя традиционные коллбеки, это может выглядеть так:

function getUser(userId, callback) {
setTimeout(() => {
console.log('Пользователь получен');
callback(null, { id: userId, name: 'John Doe' });
}, 1000);
}

function getPosts(userId, callback) {
setTimeout(() => {
console.log('Посты получены');
callback(null, [
{ postId: 1, title: 'Пост 1' },
{ postId: 2, title: 'Пост 2' }
]);
}, 1000);
}

function getComments(postId, callback) {
setTimeout(() => {
console.log('Комментарии получены');
callback(null, [
{ commentId: 1, text: 'Комментарий 1' },
{ commentId: 2, text: 'Комментарий 2' }
]);
}, 1000);
}

// callback hell
getUser(1, (err, user) => {
if (err) {
console.error(err);
} else {
getPosts(user.id, (err, posts) => {
if (err) {
console.error(err);
} else {
posts.forEach(post => {
getComments(post.postId, (err, comments) => {
if (err) {
console.error(err);
} else {
console.log(`Комментарии к посту ${post.postId}`, comments);
}
});
});
}
});
}
});

Почему так происходит?

  • Вложенная структура: Асинхронные операции зависят от результатов предыдущих операций, что приводит к вложенным коллбекам.
  • Обработка ошибок: Обработка ошибок становится громоздкой, так как на каждом уровне вложенной структуры требуется своя логика обработки ошибок.
  • Последовательное выполнение: Когда операции нужно выполнять последовательно, единственный способ обеспечить начало следующей операции после завершения предыдущей — это вложение коллбеков.

Варианты решения

До появления Промисов разработчики использовали несколько техник для смягчения ситуации вложенных коллбеков:

  • Модульность: Разбиение коллбеков на меньшие, модульные функции.
  • Именованные функции: Использование именованных функций вместо анонимных для уменьшения вложенности.
  • Библиотеки управления потоком: Использование таких библиотек, как async.js, для управления асинхронным потоком.
Модульность

Разбивка операций на меньшие функции позволяет уменьшить вложенность:

function handleComments(err, comments, postId) {
if (err) {
console.error(err);
} else {
console.log(`Комментарии к посту ${postId}`, comments);
}
}

function handlePosts(err, posts) {
if (err) {
console.error(err);
} else {
posts.forEach(post => {
getComments(post.postId, (err, comments) => handleComments(err, comments, post.postId));
});
}
}

getUser(1, (err, user) => {
if (err) {
console.error(err);
} else {
getPosts(user.id, handlePosts);
}
});
Библиотеки управления потоком

Библиотеки, такие как async.js, предоставляют абстракции для общих асинхронных шаблонов, делая код более читаемым и поддерживаемым.

Пример использования async.js:

const async = require('async');

async.waterfall([
function(callback) {
getUser(1, callback);
},
function(user, callback) {
getPosts(user.id, (err, posts) => {
if (err) return callback(err);
callback(null, posts);
});
},
function(posts, callback) {
async.each(posts, (post, cb) => {
getComments(post.postId, (err, comments) => {
if (err) return cb(err);
console.log(`Комментарии к посту ${post.postId}`, comments);
cb();
});
}, callback);
}
], function(err) {
if (err) {
console.error('Ошибка: ', err);
} else {
console.log('Готово!');
}
});

Callback hell был значительной проблемой в разработке на JavaScript до введения промисов и async/await. Хотя модульность, именованные функции и библиотеки управления потоком предоставляли некоторое облегчение, они часто приводили к коду, который все еще был далек от идеала. Эволюция к промисам значительно упростила асинхронное программирование, сделав код более читаемым, поддерживаемым и легче отлаживаемым