什么是回调地狱?

在 JavaScript 开发中,回调地狱(Callback Hell) 是指嵌套回调函数过多,导致代码呈现”金字塔”形状,严重影响可读性和可维护性的现象。

什么样的代码是回调地狱?

假设我们需要依次完成三个异步操作:

  1. 获取用户信息
  2. 根据用户获取订单列表
  3. 获取订单详情

使用回调函数实现:

1
2
3
4
5
6
7
8
getUser(userId, function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
console.log(details);
// 如果还有更多操作,继续嵌套...
});
});
});

随着业务复杂度增加,嵌套层级会越来越深,代码变得难以阅读和调试。

mui.js 中的回调地狱

在使用 mui.js 开发 App 时,mui.ajax 是常用的网络请求方法。当多个请求存在依赖关系时,很容易陷入回调地狱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 典型的 mui.ajax 回调地狱
mui.ajax('https://api.example.com/login', {
data: { username: 'admin', password: '123456' },
type: 'POST',
success: function(loginRes) {
// 登录成功后获取用户信息
mui.ajax('https://api.example.com/user/' + loginRes.userId, {
type: 'GET',
success: function(userRes) {
// 获取用户信息后查询订单
mui.ajax('https://api.example.com/orders?uid=' + userRes.id, {
type: 'GET',
success: function(ordersRes) {
// 获取订单后查询第一个订单详情
mui.ajax('https://api.example.com/order/' + ordersRes[0].id, {
type: 'GET',
success: function(detailRes) {
console.log('订单详情:', detailRes);
// 还有更多请求?继续嵌套...
},
error: function(xhr) {
console.error('获取订单详情失败');
}
});
},
error: function(xhr) {
console.error('获取订单列表失败');
}
});
},
error: function(xhr) {
console.error('获取用户信息失败');
}
});
},
error: function(xhr) {
console.error('登录失败');
}
});

这段代码存在明显问题:

  • 嵌套层级深,难以阅读
  • 错误处理分散,不好维护
  • 调试困难,难以追踪数据流

封装 Promise 解决 mui.ajax 回调地狱

我们可以将 mui.ajax 封装成 Promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 封装 mui.ajax 为 Promise
function request(url, options = {}) {
return new Promise((resolve, reject) => {
mui.ajax(url, {
data: options.data || {},
type: options.type || 'GET',
dataType: options.dataType || 'json',
success: resolve,
error: (xhr, type, errorThrown) => {
reject(new Error(errorThrown || '请求失败'));
}
});
});
}

然后使用 async/await 重写上面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async function getOrderDetail() {
try {
// 登录
const loginRes = await request('https://api.example.com/login', {
type: 'POST',
data: { username: 'admin', password: '123456' }
});

// 获取用户信息
const userRes = await request('https://api.example.com/user/' + loginRes.userId);

// 获取订单列表
const ordersRes = await request('https://api.example.com/orders?uid=' + userRes.id);

// 获取订单详情
const detailRes = await request('https://api.example.com/order/' + ordersRes[0].id);

console.log('订单详情:', detailRes);
return detailRes;
} catch (error) {
console.error('请求出错:', error.message);
mui.toast(error.message);
}
}

getOrderDetail();

代码从嵌套变成了线性结构,清晰易读,错误处理也集中在一处。

Vue 中的回调地狱

在 Vue 项目中使用 axios 时,如果不注意也会写出回调地狱式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Vue 组件中的回调地狱(错误示范)
export default {
methods: {
loadData() {
this.$axios.post('/api/login', this.loginForm).then(res => {
if (res.data.code === 200) {
this.$axios.get('/api/user/' + res.data.userId).then(userRes => {
if (userRes.data.code === 200) {
this.userInfo = userRes.data.data;
this.$axios.get('/api/orders', { params: { uid: this.userInfo.id } }).then(orderRes => {
if (orderRes.data.code === 200) {
this.orders = orderRes.data.data;
this.$axios.get('/api/order/' + this.orders[0].id).then(detailRes => {
this.orderDetail = detailRes.data.data;
}).catch(err => {
this.$message.error('获取订单详情失败');
});
}
}).catch(err => {
this.$message.error('获取订单列表失败');
});
}
}).catch(err => {
this.$message.error('获取用户信息失败');
});
}
}).catch(err => {
this.$message.error('登录失败');
});
}
}
}

Vue 中使用 async/await 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Vue 组件中的正确写法
export default {
methods: {
async loadData() {
try {
// 登录
const loginRes = await this.$axios.post('/api/login', this.loginForm);
if (loginRes.data.code !== 200) throw new Error('登录失败');

// 获取用户信息
const userRes = await this.$axios.get('/api/user/' + loginRes.data.userId);
this.userInfo = userRes.data.data;

// 获取订单列表
const orderRes = await this.$axios.get('/api/orders', {
params: { uid: this.userInfo.id }
});
this.orders = orderRes.data.data;

// 获取订单详情
const detailRes = await this.$axios.get('/api/order/' + this.orders[0].id);
this.orderDetail = detailRes.data.data;

} catch (error) {
this.$message.error(error.message || '请求失败');
}
}
}
}

uni-app 中的回调地狱

uni-app 的 uni.request 默认使用回调方式,很容易写成回调地狱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// uni-app 回调地狱(错误示范)
uni.request({
url: 'https://api.example.com/login',
method: 'POST',
data: { username: 'admin', password: '123456' },
success: (loginRes) => {
if (loginRes.data.code === 200) {
uni.request({
url: 'https://api.example.com/user/' + loginRes.data.userId,
success: (userRes) => {
uni.request({
url: 'https://api.example.com/orders',
data: { uid: userRes.data.id },
success: (orderRes) => {
uni.request({
url: 'https://api.example.com/order/' + orderRes.data[0].id,
success: (detailRes) => {
console.log('订单详情:', detailRes.data);
},
fail: () => uni.showToast({ title: '获取详情失败', icon: 'none' })
});
},
fail: () => uni.showToast({ title: '获取订单失败', icon: 'none' })
});
},
fail: () => uni.showToast({ title: '获取用户失败', icon: 'none' })
});
}
},
fail: () => uni.showToast({ title: '登录失败', icon: 'none' })
});

封装 uni.request 为 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// utils/request.js
export function request(options) {
return new Promise((resolve, reject) => {
uni.request({
...options,
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(new Error(res.data.message || '请求失败'));
}
},
fail: (err) => {
reject(new Error(err.errMsg || '网络错误'));
}
});
});
}

uni-app 中使用 async/await 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { request } from '@/utils/request.js';

export default {
methods: {
async loadOrderDetail() {
try {
uni.showLoading({ title: '加载中...' });

// 登录
const loginRes = await request({
url: 'https://api.example.com/login',
method: 'POST',
data: { username: 'admin', password: '123456' }
});

// 获取用户信息
const userRes = await request({
url: 'https://api.example.com/user/' + loginRes.userId
});

// 获取订单列表
const orderRes = await request({
url: 'https://api.example.com/orders',
data: { uid: userRes.id }
});

// 获取订单详情
const detailRes = await request({
url: 'https://api.example.com/order/' + orderRes[0].id
});

this.orderDetail = detailRes;

} catch (error) {
uni.showToast({ title: error.message, icon: 'none' });
} finally {
uni.hideLoading();
}
}
}
}

💡 提示:uni-app 从 2.0 版本开始支持 uni.request 返回 Promise,可以直接使用 await uni.request(),但建议还是封装一层统一处理错误和 loading。

为什么会产生回调地狱?

原因 说明
多层依赖 后续操作依赖前一个异步结果
缺乏抽象 没有将逻辑拆分成独立函数
串行执行 操作必须按顺序执行,无法并行

解决方案

1. Promise 链式调用

Promise 通过 .then() 链式调用,将嵌套变成扁平结构:

1
2
3
4
5
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(error => console.error(error));

2. async/await(推荐)

ES2017 引入的语法糖,让异步代码看起来像同步代码:

1
2
3
4
5
6
7
8
9
10
11
12
async function fetchOrderDetails(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
console.log(details);
} catch (error) {
console.error(error);
}
}

fetchOrderDetails(userId);

3. 并行优化

如果多个异步操作互不依赖,可以用 Promise.all 并行执行:

1
2
3
4
5
6
7
8
async function fetchData() {
const [users, products, orders] = await Promise.all([
getUsers(),
getProducts(),
getOrders()
]);
console.log(users, products, orders);
}

总结

方案 优点 适用场景
Promise 链式调用,避免嵌套 简单异步流程
async/await 代码直观,易于调试 复杂异步逻辑(推荐)
Promise.all 并行执行,提升性能 多个独立异步操作

现代 JavaScript 开发中,推荐使用 async/await 处理异步操作,代码更清晰,调试更方便。