09-06-24 16:23:49
Эта ситуация возникает, когда несколько асинхронных операций вложены друг в друга, создавая глубокую, пирамидальную структуру коллбеков. Такое вложение делает код трудным для чтения, отладки и поддержки.
Пример
Рассмотрим сценарий, в котором вам нужно выполнить серию асинхронных операций: получить данные пользователя, затем посты этого пользователя, а затем комментарии к каждому посту. Используя традиционные коллбеки, это может выглядеть так:
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);
}
});
});
}
});
}
});
До появления Промисов разработчики использовали несколько техник для смягчения ситуации вложенных коллбеков:
Разбивка операций на меньшие функции позволяет уменьшить вложенность:
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. Хотя модульность, именованные функции и библиотеки управления потоком предоставляли некоторое облегчение, они часто приводили к коду, который все еще был далек от идеала. Эволюция к промисам значительно упростила асинхронное программирование, сделав код более читаемым, поддерживаемым и легче отлаживаемым