目录
回调函数地狱与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
七、总结
回调函数地狱与Promise链式调用
一、回调函数地狱
回调函数地狱是 JavaScript 异步编程早期面临的典型问题,表现为多层嵌套的回调函数,导致代码难以阅读和维护。
1. 典型场景示例
需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中。
使用回调函数实现:
// 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
})
})
})
在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱。
2. 回调地狱的问题
-
代码金字塔:嵌套层级深,形成“向右倾倒”的金字塔结构,可读性差。
-
错误处理冗余:每个回调需单独处理错误,代码重复。
-
流程控制困难:难以实现复杂逻辑(如并行任务、条件分支),耦合性严重。
二、Promise链式调用
Promise 通过链式调用(Chaining)解决了回调地狱问题,将嵌套结构转为扁平化的流水线式代码。
链式调用:利用 then 方法返回新 Promise 对象特性,一直串联下去。
1. 链式调用解决回调地狱
将上述回调地狱改写为链式调用:
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 的回调函数中 return Promise对象,影响当前新 Promise 对象的值。
2. 链式调用的核心规则
-
值传递:每个
.then()
接收前一个 Promise 的结果。 -
返回新 Promise:
.then()
回调中可返回新 Promise,继续链式调用。 -
错误冒泡:链中任何位置的错误都会传递到最近的
.catch()
。
三、链式调用深度解析
1. 链式调用本质
每个 .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 对象。
2. 错误处理机制
-
统一捕获:通过一个
.catch()
捕获链中所有错误。 -
中断链式:一旦触发错误,后续
.then()
会被跳过,直接跳转至.catch()
。 -
恢复链式:在
.catch()
后仍可继续.then()
。
四、回调地狱 vs 链式调用
特性 | 回调函数 | Promise 链式调用 |
---|---|---|
代码结构 | 嵌套层级深,可读性差 | 扁平化链式,逻辑清晰 |
错误处理 | 每个回调单独处理,冗余 | 统一通过 .catch() 捕获 |
流程控制 | 难以实现复杂逻辑(如并行、条件分支) | 结合 Promise.all 、async/await 更灵活 |
调试难度 | 堆栈信息不完整,难以追踪 | 错误冒泡机制,堆栈更清晰 |
复用性 | 回调函数耦合度高,复用困难 | 每个 .then() 可独立封装,复用性强 |
五、高级链式技巧
1. 条件分支
fetchUser()
.then(user => {
if (user.isVIP) {
return fetchVIPContent(user.id); // 返回新 Promise
} else {
return fetchBasicContent(); // 返回普通值
}
})
.then(content => {
console.log('内容:', content);
});
2. 并行任务
结合 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);
});
3. 链式中断
通过返回 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
async/await
是 JavaScript 处理异步操作的语法糖,基于 Promise 实现,旨在让异步代码的写法更接近同步逻辑,彻底解决回调地狱问题。概念: 在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象状态的结果值 。
一、async 函数
-
定义与特性
-
语法:在函数前添加
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 表达式
-
作用与规则
-
语法:
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/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
统一处理同步和异步错误。
四、高级用法
1. 并行执行异步任务
-
顺序执行(效率低):
const user = await fetchUser(); // 先执行 const posts = await fetchPosts(); // 后执行(等待 user 完成)
-
并行执行(效率高):
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
2. 循环中的 await
-
错误示例(顺序执行,耗时长):
for (const url of urls) { await fetch(url); // 每个请求等待上一个完成 }
-
正确示例(并行触发):
const promises = urls.map(url => fetch(url)); const results = await Promise.all(promises);
3. 顶层 await
-
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
优化。
-
六、链式调用 vs async/await
特性 | Promise 链式调用 | async/await |
---|---|---|
代码结构 | 链式 .then() ,需处理嵌套 | 类似同步代码,无嵌套 |
错误处理 | 通过 .catch() 或链式参数 | 使用 try/catch 统一处理 |
底层机制 | 直接操作 Promise 链 | 基于生成器和 Promise 的语法糖 |
可读性 | 简单链式清晰,复杂场景混乱 | 逻辑直观,适合复杂异步流程 |
调试体验 | 错误堆栈可能跨多个 .then() | 错误堆栈更贴近代码行号 |
七、总结
-
核心优势:
-
代码扁平化,更接近同步逻辑的直观性。
-
错误处理更统一(
try/catch
覆盖同步和异步错误)。
-
-
适用场景:
-
需要顺序执行的异步任务(如依次请求 A → B → C)。
-
复杂异步流程(需结合条件判断、循环等)。
-
-
注意事项:
-
避免滥用导致性能问题(如无必要的顺序执行)。
-
在非模块环境中,顶层
await
需封装在async
函数中。
-