回调地狱及解决方法
什么是回调地狱?
回调地狱(Callback Hell)是指在JavaScript异步编程中,多层嵌套的回调函数导致的代码难以理解和维护的现象。在Axios请求中(then的回调),这种问题尤为常见。
典型Axios回调地狱示例
想要获取省市区三个数据:
axios({ url: 'http://hmajax.itheima.net/api/province' }).then(result => {const pname = result.data.list[0]document.querySelector('.province').innerHTML = pname// 2. 获取默认第一个城市的名字axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } }).then(result => {const cname = result.data.list[0]document.querySelector('.city').innerHTML = cname// 3. 获取默认第一个地区的名字axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } }).then(result => {console.log(result)const areaName = result.data.list[0]document.querySelector('.area').innerHTML = areaName})})
}).catch(error => {console.dir(error)
})
这段代码的问题
- 金字塔式缩进:嵌套层级过深,代码向右延伸
- 错误处理不完善:内层请求的错误无法被外层catch捕获
- 变量污染:pname、cname等变量在多层作用域中传递
- 可维护性差:添加新请求或修改逻辑困难
解决方案一:Promise链式调用改造
优化后的Promise链式版本
axios({ url: 'http://hmajax.itheima.net/api/province' }).then(provinceResult => {const pname = provinceResult.data.list[0]document.querySelector('.province').innerHTML = pnamereturn axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })}).then(cityResult => {const cname = cityResult.data.list[0]document.querySelector('.city').innerHTML = cnamereturn axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname: document.querySelector('.province').innerHTML,cname }})}).then(areaResult => {const areaName = areaResult.data.list[0]document.querySelector('.area').innerHTML = areaName}).catch(error => {console.error('请求出错:', error)// 可以在这里统一显示错误提示document.querySelector('.error').innerHTML = '加载失败,请重试'})
优化点分析
- 扁平化结构:从3层嵌套变为1层链式调用
- 统一错误处理:单个catch捕获所有可能的错误,出现错误剩余部分不会执行
- 变量传递清晰:通过return传递数据到下一个then
- 可扩展性强:易于添加新的请求步骤
解决方案二:Async/Await
使用Async/Await重构
async function loadRegionData() {try {// 1. 加载省份数据const provinceRes = await axios({ url: 'http://hmajax.itheima.net/api/province' })const pname = provinceRes.data.list[0]document.querySelector('.province').innerHTML = pname// 2. 加载城市数据const cityRes = await axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })const cname = cityRes.data.list[0]document.querySelector('.city').innerHTML = cname// 3. 加载区域数据const areaRes = await axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } })const areaName = areaRes.data.list[0]document.querySelector('.area').innerHTML = areaName} catch (error) {console.error('加载地区数据失败:', error)// 统一错误处理alert('数据加载失败,请刷新重试')}
}// 执行函数
loadRegionData()
Async/Await的优势
- 同步式写法:代码从上到下顺序执行,符合直觉
- 变量作用域清晰:所有变量都在同一作用域内
- 错误处理简单:使用try-catch块包裹所有请求
- 调试方便:可以在任意位置添加断点
高级技巧:并行请求优化
当请求之间没有依赖关系时,可以使用Promise.all进行并行请求:
async function loadAllData() {try {const [provinceRes, cityRes, areaRes] = await Promise.all([axios({ url: 'http://hmajax.itheima.net/api/province' }),axios({ url: 'http://hmajax.itheima.net/api/city' }),axios({ url: 'http://hmajax.itheima.net/api/area' })])// 处理结果document.querySelector('.province').innerHTML = provinceRes.data.list[0]document.querySelector('.city').innerHTML = cityRes.data.list[0]document.querySelector('.area').innerHTML = areaRes.data.list[0]} catch (error) {console.error('并行加载失败:', error)}
}