跳到主要内容

关于js中如何避免回调地狱的问题

· 阅读需 7 分钟
兔兔
兔兔

很多js新人———嗯,也包括我,在js里第一次写东西的时候,接触到了axios,和很多萌新一样,我最早学会的js请求就只会最基本的回调函数:

axios.get('/api/data')
.then(response => {
// 处理数据
const data = response.data;
return anotherRequest(data.id); //调用数据
})
.then(secondResponse => {
// 嵌套层级增加......
})
.catch(error => {
// 错误处理
});

这样搞多了,复杂点的逻辑就会变成灾难性的回调地狱,加一个请求就得从头开始捋...


axios.get(`${API_URL}/users/1`)
.then(response => {
console.log('1. 获取用户信息:', response.data.name);

// 获取该用户的帖子
axios.get(`${API_URL}/posts?userId=${response.data.id}`)
.then(postsResponse => {
console.log('2. 获取用户帖子:', postsResponse.data.length);

// 获取第一个帖子的评论
axios.get(`${API_URL}/posts/${postsResponse.data[0].id}/comments`)
.then(commentsResponse => {
console.log('3. 获取帖子评论:', commentsResponse.data.length);

// 获取第一条评论的作者信息
axios.get(`${API_URL}/users/${commentsResponse.data[0].id}`)
.then(userResponse => {
console.log('4. 获取评论作者:', userResponse.data.name);

// 获取该作者的相册
axios.get(`${API_URL}/albums?userId=${userResponse.data.id}`)
.then(albumsResponse => {
console.log('5. 获取作者相册:', albumsResponse.data.length);

// 结果
console.log('6. 最终结果:', {
originalUser: response.data.name,
postCount: postsResponse.data.length,
commentCount: commentsResponse.data.length,
commentAuthor: userResponse.data.name,
albumCount: albumsResponse.data.length
});
})
.catch(err => console.error('获取相册失败:', err));
})
.catch(err => console.error('获取评论作者失败:', err));
})
.catch(err => console.error('获取评论失败:', err));
})
.catch(err => console.error('获取帖子失败:', err));
})
.catch(err => console.error('获取用户失败:', err));

真是看的人想撞墙,不是吗?如果你不系统地学习js,而是像我一样作为一个业余爱好者,学来处理一些基础的业务,很容易写出这样的东西来。

实际上,现在不应当这么麻烦的。现在我们可以使用ES2017引入的async/await方式,来使异步代码看起来更像同步代码,从而提高可读性。通过使用async函数,我们可以在函数内部使用await关键字来等待Promise的解决,这样代码结构更清晰,更易于维护。

async function fetchUserData() {
try {
// 1. 获取用户信息
const userResponse = await axios.get(`${API_URL}/users/1`);
console.log('1. 获取用户信息:', userResponse.data.name);

// 2. 获取该用户的帖子
const postsResponse = await axios.get(`${API_URL}/posts?userId=${userResponse.data.id}`);
console.log('2. 获取用户帖子:', postsResponse.data.length);

// 3. 获取第一个帖子的评论
const commentsResponse = await axios.get(`${API_URL}/posts/${postsResponse.data[0].id}/comments`);
console.log('3. 获取帖子评论:', commentsResponse.data.length);

// 4. 获取第一条评论的作者信息
const commentAuthorResponse = await axios.get(`${API_URL}/users/${commentsResponse.data[0].id}`);
console.log('4. 获取评论作者:', commentAuthorResponse.data.name);

// 5. 获取该作者的相册
const albumsResponse = await axios.get(`${API_URL}/albums?userId=${commentAuthorResponse.data.id}`);
console.log('5. 获取作者相册:', albumsResponse.data.length);

// 6. 最终结果
const result = {
originalUser: userResponse.data.name,
postCount: postsResponse.data.length,
commentCount: commentsResponse.data.length,
commentAuthor: commentAuthorResponse.data.name,
albumCount: albumsResponse.data.length
};

console.log('6. 最终结果:', result);
return result;

} catch (error) {
console.error('请求过程中发生错误:', error.message);
throw error;
}
}

这样,回调地狱消失了,程序变得清晰整洁。

这时候就会有人问了,分开处理了所有的业务,这样性能会受到影响吗,一定程度确实会。我们的程序还有优化的空间:我们可以使用Promise.all,把两个变量放在一起并行请求,节省时间:


async function fetchOptimizedData() {
try {
// 1. 获取用户基本信息 (并行获取用户和用户帖子)
const [userResponse, postsResponse] = await Promise.all([
axios.get(`${API_URL}/users/1`),
axios.get(`${API_URL}/posts?userId=1`)
]);

console.log('1. 获取用户信息:', userResponse.data.name);
console.log('2. 获取用户帖子:', postsResponse.data.length);

// 2. 获取第一个帖子的评论和第一条评论的作者信息 (并行)
const [commentsResponse, commentAuthorResponse] = await Promise.all([
axios.get(`${API_URL}/posts/${postsResponse.data[0].id}/comments`),
axios.get(`${API_URL}/users/${userResponse.data.id}`) // 假设这里需要获取的是评论作者
]);

console.log('3. 获取帖子评论:', commentsResponse.data.length);
console.log('4. 获取评论作者:', commentAuthorResponse.data.name);

// 3. 获取该作者的相册和待办事项 (并行)
const [albumsResponse, todosResponse] = await Promise.all([
axios.get(`${API_URL}/albums?userId=${commentAuthorResponse.data.id}`),
axios.get(`${API_URL}/todos?userId=${commentAuthorResponse.data.id}`)
]);

console.log('5. 获取作者相册:', albumsResponse.data.length);
console.log('6. 获取作者待办事项:', todosResponse.data.length);

// 返回最终结果
return {
user: userResponse.data,
postCount: postsResponse.data.length,
commentCount: commentsResponse.data.length,
commentAuthor: commentAuthorResponse.data,
albumCount: albumsResponse.data.length,
todoCount: todosResponse.data.length
};

} catch (error) {
console.error('请求过程中发生错误:', error.message);
throw error;
}
}

这样,原本的时间为"请求1+请求2"的时间,现在并行请求就变成了每一次请求中,最慢的那个函数的时间。

有的人觉得这样等待,不会使得被堵塞吗?并不会。这个函数是在程序中统一async调用。await仅暂停​​当前 async 函数​​,并不会阻塞主线程,因为底层基于 Promise,本质仍是异步,不会冻结UI等等;

当然还有最后一个问题:每一次请求都只有获取了,从而报错统一处理。忘记了某一次就会导致未处理 rejection。这个时候建议使用全局的错误监听,这样遇到了也不会导致未处理的错误导致崩溃。

// 浏览器全局错误监听
window.addEventListener('unhandledrejection', (event) => {
// 阻止默认行为(控制台报错)
event.preventDefault();

console.error('[浏览器] 未处理的 Promise 拒绝:', event.reason);

// 可在此处添加其他逻辑(比如说上报?
reportErrorToServer(event.reason);
});

// 以及Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('[Node.js] 未处理的 Promise 拒绝:', reason);

// 可在此处添加其他逻辑(比如说上报?
reportErrorToServer(reason);

// 防止进程崩溃(看情况)
// process.exit(1); // 严重错误时就直接及时止损(爆!)
});

优雅的代码使人事半功倍,这就是我从中得到的最大的收获。