目录
回调函数地狱与Promise链式调用
一、回调函数地狱
1. 典型场景示例
2. 回调地狱的问题
二、Promise链式调用
1. 链式调用解决回调地狱
2. 链式调用的核心规则
三、链式调用深度解析
1. 链式调用本质
2. 错误处理机制
四、回调地狱 vs 链式调用
五、高级链式技巧
1. 条件分支
2. 并行任务
3. 链式中断
六、总结
async 和 await
一、async 函数
二、await 表达式
三、async/await解决回调地狱
四、高级用法
1. 并行执行异步任务
2. 循环中的 await
3. 顶层 await
五、常见问题与解决方案
六、链式调用 vs async/await
七、总结
回调函数地狱是 JavaScript 异步编程早期面临的典型问题,表现为多层嵌套的回调函数,导致代码难以阅读和维护。
需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中。
使用回调函数实现:
// 1. 获取默认第一个省份的名字
axios({ url: 'http://ajax.net/api/province' })
.then(result => {
const pname = result.data.list[0]
document.querySelector('.province').innerHTML = pname
// 2. 获取默认第一个城市的名字
axios({ url: 'http://ajax.net/api/city', params: { pname } })
.then(result => {
const cname = result.data.list[0]
document.querySelector('.city').innerHTML = cname
// 3. 获取默认第一个地区的名字
axios({ url: 'http://ajax.net/api/area', params: { pname, cname } })
.then(result => {
console.log(result)
const areaName = result.data.list[0]
document.querySelector('.area').innerHTML = areaName
})
})
})
在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱。
代码金字塔:嵌套层级深,形成“向右倾倒”的金字塔结构,可读性差。
错误处理冗余:每个回调需单独处理错误,代码重复。
流程控制困难:难以实现复杂逻辑(如并行任务、条件分支),耦合性严重。
Promise 通过链式调用(Chaining)解决了回调地狱问题,将嵌套结构转为扁平化的流水线式代码。
链式调用:利用 then 方法返回新 Promise 对象特性,一直串联下去。
将上述回调地狱改写为链式调用:
let pname = ''
// 1. 得到-获取省份Promise对象
axios({ url: 'http://hmajax.itheima.net/api/province' })
.then(result => {
pname = result.data.list[0]
document.querySelector('.province').textContent = pname
// 2. 得到-获取城市Promise对象
return axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })
})
.then(result => {
const cname = result.data.list[0]
document.querySelector('.city').textContent = cname
// 3. 得到-获取地区Promise对象
return axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } })
})
.then(result => {
const aname = result.data.list[0]
document.querySelector('.area').textContent = aname
})
.catch(error => {
console.log(error)
})
Promise 链式调用如何解决回调函数地狱?
值传递:每个 .then()
接收前一个 Promise 的结果。
返回新 Promise:.then()
回调中可返回新 Promise,继续链式调用。
错误冒泡:链中任何位置的错误都会传递到最近的 .catch()
。
每个 .then()
会返回 新的 Promise 对象,其状态由回调函数决定:
若回调返回非 Promise 值 → 新 Promise 直接成功(Fulfilled
)。
Promise.resolve(1)
.then(n => n + 2) // 返回 3(普通值)
.then(console.log); // 输出 3
若回调返回 Promise → 新 Promise 与其状态同步。
Promise.resolve(1)
.then(n => Promise.resolve(n + 2)) // 返回新 Promise
.then(console.log); // 输出 3
若回调抛出错误 → 新 Promise 失败(Rejected
)。
Promise.resolve(1)
.then(() => { throw new Error('Fail') })
.catch(console.error); // 捕获错误
在 then 回调函数中,return 的值会传给 then 方法生成的新 Promise 对象。
统一捕获:通过一个 .catch()
捕获链中所有错误。
中断链式:一旦触发错误,后续 .then()
会被跳过,直接跳转至 .catch()
。
恢复链式:在 .catch()
后仍可继续 .then()
。
特性 | 回调函数 | Promise 链式调用 |
---|---|---|
代码结构 | 嵌套层级深,可读性差 | 扁平化链式,逻辑清晰 |
错误处理 | 每个回调单独处理,冗余 | 统一通过 .catch() 捕获 |
流程控制 | 难以实现复杂逻辑(如并行、条件分支) | 结合 Promise.all 、async/await 更灵活 |
调试难度 | 堆栈信息不完整,难以追踪 | 错误冒泡机制,堆栈更清晰 |
复用性 | 回调函数耦合度高,复用困难 | 每个 .then() 可独立封装,复用性强 |
fetchUser()
.then(user => {
if (user.isVIP) {
return fetchVIPContent(user.id); // 返回新 Promise
} else {
return fetchBasicContent(); // 返回普通值
}
})
.then(content => {
console.log('内容:', content);
});
结合 Promise.all
实现并行:
const fetchUser = axios.get('/api/user');
const fetchPosts = axios.get('/api/posts');
Promise.all([fetchUser, fetchPosts])
.then(([user, posts]) => {
console.log('用户:', user.data, '帖子:', posts.data);
});
通过返回 Promise.reject()
主动中断链式:
login()
.then(token => {
if (!tokenValid(token)) {
return Promise.reject(new Error('Token 无效')); // 主动中断
}
return getUserInfo(token);
})
.catch(error => {
console.error('流程中断:', error);
});
回调地狱是早期异步编程的痛点,代码臃肿且难以维护。
Promise 链式调用通过扁平化结构和错误冒泡机制,极大提升了代码可读性和可维护性。
最佳实践:
优先使用 Promise 链式替代嵌套回调。
结合 async/await
语法糖进一步简化异步代码。
善用 Promise.all
、Promise.race
等工具处理复杂场景。
async/await
是 JavaScript 处理异步操作的语法糖,基于 Promise 实现,旨在让异步代码的写法更接近同步逻辑,彻底解决回调地狱问题。概念: 在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象状态的结果值 。
定义与特性
语法:在函数前添加 async
关键字,表示该函数包含异步操作。
返回值:始终返回一个 Promise 对象:
若函数返回非 Promise 值,会自动包装为 Promise.resolve(value)
。
若抛出错误,返回 Promise.reject(error)
。
async function fetchData() {
return 'Hello World'; // 等价于 Promise.resolve('Hello World')
}
fetchData().then(console.log); // 输出 "Hello World"
错误处理
在 async
函数内部使用 try/catch
捕获同步或异步错误。
async function fetchWithError() {
try {
const data = await axios({ url: 'invalid-url' });
} catch (error) {
console.error('捕获错误:', error); // 网络错误或 Promise 拒绝
}
}
fetchWithError()
作用与规则
语法:await
后接一个 Promise 对象(或原始值)。
行为:
暂停当前 async
函数的执行,等待 Promise 完成。
若 Promise 成功,返回其解决的值。
若 Promise 拒绝,抛出拒绝的原因(需用 try/catch
捕获)。
限制:await
只能在 async
函数内部使用。
async function getUser() {
const response = await fetch('/api/user'); // 等待 fetch 完成
const data = await response.json(); // 等待 JSON 解析
return data;
}
执行顺序
同步代码优先:await
不会阻塞函数外的代码。
async function demo() {
console.log(1);
await Promise.resolve(); // 暂停此处,但外部代码继续执行
console.log(2);
}
demo();
console.log(3);
// 输出顺序: 1 → 3 → 2
将上述回调地狱改写为async/await:
async function getData() {
try {
const pObj = await axios({ url: 'http://ajax.net/api/province' })
const pname = pObj.data.list[0]
const cObj = await axios({ url: 'http://ajax.net/api/city', params: { pname } })
const cname = cObj.data.list[0]
const aObj = await axios({ url: 'http://ajax.net/api/area', params: { pname, cname } })
const aname = aObj.data.list[0]
document.querySelector('.province').innerHTML = pname
document.querySelector('.city').innerHTML = cname
document.querySelector('.area').innerHTML = aname
} catch (error) {
console.log(error)
}
}
getData()
错误处理:
Promise:依赖 .catch()
或 .then()
的第二个参数。
async/await:使用 try/catch
统一处理同步和异步错误。
顺序执行(效率低):
const user = await fetchUser(); // 先执行
const posts = await fetchPosts(); // 后执行(等待 user 完成)
并行执行(效率高):
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
错误示例(顺序执行,耗时长):
for (const url of urls) {
await fetch(url); // 每个请求等待上一个完成
}
正确示例(并行触发):
const promises = urls.map(url => fetch(url));
const results = await Promise.all(promises);
ES2022+ 支持在模块的顶层作用域使用 await
。
// 模块中直接使用
const data = await fetchData();
console.log(data);
忘记 await
现象:函数返回 Promise 而非预期值。
解决:确保异步操作前添加 await
。
async function demo() {
const data = fetch('/api'); // 错误!缺少 await
console.log(data); // 输出 Promise 对象
}
未捕获的错误
现象:未使用 try/catch
导致未处理的 Promise 拒绝。
解决:始终用 try/catch
包裹 await
,或在函数调用后加 .catch()
。
async function riskyTask() {
await dangerousOperation();
}
riskyTask().catch(console.error); // 捕获未处理的错误
性能陷阱
现象:不必要的顺序执行降低性能。
解决:合理使用 Promise.all
或 Promise.race
优化。
特性 | Promise 链式调用 | async/await |
---|---|---|
代码结构 | 链式 .then() ,需处理嵌套 |
类似同步代码,无嵌套 |
错误处理 | 通过 .catch() 或链式参数 |
使用 try/catch 统一处理 |
底层机制 | 直接操作 Promise 链 | 基于生成器和 Promise 的语法糖 |
可读性 | 简单链式清晰,复杂场景混乱 | 逻辑直观,适合复杂异步流程 |
调试体验 | 错误堆栈可能跨多个 .then() |
错误堆栈更贴近代码行号 |
核心优势:
代码扁平化,更接近同步逻辑的直观性。
错误处理更统一(try/catch
覆盖同步和异步错误)。
适用场景:
需要顺序执行的异步任务(如依次请求 A → B → C)。
复杂异步流程(需结合条件判断、循环等)。
注意事项:
避免滥用导致性能问题(如无必要的顺序执行)。
在非模块环境中,顶层 await
需封装在 async
函数中。