在 JavaScript 开发中,回调地狱(Callback Hell) 是指嵌套回调函数过多,导致代码呈现”金字塔”形状,严重影响可读性和可维护性的现象。
什么样的代码是回调地狱?
假设我们需要依次完成三个异步操作:
- 获取用户信息
- 根据用户获取订单列表
- 获取订单详情
使用回调函数实现:
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('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
| 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
| 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
| 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.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
| 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 处理异步操作,代码更清晰,调试更方便。