主题
async 与 await
本页对应 1-11async和await,共 6 段视频,约 1 小时 2 分。
学习目标
- 能够根据给定代码判断其同步或异步执行顺序,并说明定时器、事件监听、网络请求等异步代码的结果接收方式。
- 能够识别回调函数嵌套形成的回调地狱结构,并说明其在可读性和异常捕获方面的主要问题。
- 能够使用 Promise 的
then链式调用改写多层回调嵌套代码,并通过return正确衔接下一个异步任务。 - 能够使用
async声明异步函数,并使用await获取 Promise 成功结果,将链式调用代码改写为更简洁的平级写法。 - 能够在
async函数中使用try...catch捕获异步请求错误,并读取错误信息进行相应处理。 - 能够独立完成“获取省、市、区数据并渲染第一项”的案例,并分别用 Promise 链式调用和
async/await两种方式实现。
知识点
先明确目标:为什么要区分同步和异步
写 JavaScript 时,代码不一定是“从上到下立刻全部执行完”的。知道哪些代码会原地等待、哪些代码会先跳过、结果什么时候回来,直接决定我们能不能正确写逻辑、能不能看懂控制台打印顺序、能不能调试问题。比如定时器、事件、AJAX 请求,如果误以为它们会像普通赋值一样立刻完成,就会写出顺序错误的代码。
判断时先问自己:这段代码调用后,是否必须原地等结果回来,才能继续往下执行?如果是,就是同步代码;如果调用后耗时,但不阻塞后续代码,而是在将来完成后触发回调函数传递结果,就是异步代码。
同步代码:逐行执行,原地等待结果
同步代码的特点是逐行执行,上一行完成后才执行下一行,需要原地等待结果后再继续向下执行。绝大多数普通语句、变量声明、表达式计算都是同步代码。
怎么做?看下面代码:
javascript
/**
* 1. 同步代码
* */
console.log(1)
const num = 1 + 1
console.log(num)执行过程很直接:先打印 1,再计算 1 + 1 得到 2 并赋值给 num,最后打印 num,所以输出是 1、2。这里没有任何异步任务,不需要等定时器、事件或网络请求。
易错点:不要把“代码行数多”误认为异步。只要没有定时器、事件注册、AJAX 等异步操作,普通代码仍然按同步顺序执行。
异步代码:调用后耗时,但不阻塞后续执行
异步代码的特点是:调用后可能耗时,但不会阻塞代码继续执行,不必原地等待;将来完成后会触发回调函数,把结果交给回调函数处理。常见异步代码有 setTimeout / setInterval、事件、AJAX(XHR)等。
怎么想?看到“将来才发生”的操作,就要考虑异步:等一秒钟后执行、等用户点击后执行、等服务器响应后执行。它们都不是立刻拿到结果,而是先注册回调,条件满足后再执行回调。
先看定时器示例:
javascript
/**
* 2. 同步+异步(定时器)
* */
console.log(1)
setTimeout(() => {
console.log(2)
}, 1000)
console.log(3)这段代码先打印 1。遇到 setTimeout 时,只是注册了一个定时器,并不会在这里停住等一秒;所以继续执行后面的 console.log(3),先打印 3。大约 1 秒后,定时器完成,触发回调函数,再打印 2。因此顺序是:先 1、3,约 1 秒后 2。
易错点:setTimeout 后面的 1000 是延迟时间,不是让同步代码原地等 1000 毫秒。异步代码“看起来写在中间”,不代表它会在中间立刻执行。
事件和 XHR:注册回调不等于立刻执行
事件监听也是异步代码。注册事件时,只是告诉浏览器“将来满足条件时执行这个回调”,并不会立刻执行。
javascript
/**
* 3. 同步+异步(事件)
* */
console.log(1)
document.body.addEventListener('click', () => {
console.log(2)
})
console.log(3)执行时先打印 1,然后注册点击事件,再打印 3。此时不会打印 2,因为用户还没有点击。只有点击 document.body 后,事件触发,回调函数执行,才打印 2。所以初始顺序是 1、3;点击后输出 2。
XHR 请求同理:
javascript
/**
* 4. 同步+异步(XHR)
* */
console.log(1)
const xhr = new XMLHttpRequest()
xhr.open('get', 'url地址')
xhr.addEventListener('loadend', () => {
console.log(2)
})
xhr.send()
console.log(3)先打印 1。创建 XHR、设置请求方法、注册 loadend 回调、调用 send() 发送请求后,代码不会停在 send() 等服务器响应,而是继续执行,先打印 3。等服务器内容响应回来,触发 loadend 回调,再打印 2。因此顺序是:先 1、3,服务器响应回来后 2。
这里可以结合调试过程理解:如果网络很慢,请求可能很久才回来;如果代码停在请求处不往后走,后续页面交互、事件绑定等逻辑都无法执行,用户体验会很差。所以 XHR 的 loadend 回调属于异步代码,不会阻塞后续执行。
易错点:注册事件不等于触发事件;send() 发出请求不等于立刻拿到响应;异步结果不能在 send() 后面直接当同步结果使用。
小结:异步结果靠回调函数接收
回答打印顺序时,核心方法是:先执行同步代码,遇到异步代码先“注册/发起”,不原地等待;等异步任务完成条件满足后,再执行对应回调函数。本节中定时器、事件、XHR 接收结果使用的都是回调函数。
最后记住四个判断点:同步代码逐行执行、原地等待结果;异步代码调用后耗时、不阻塞后续执行;常见异步代码包括 setTimeout / setInterval、事件、AJAX;异步代码的结果依靠回调函数接收。
从需求理解:为什么要认识回调函数地狱
这一节的目标是了解回调函数地狱的概念和缺点。先看一个真实需求:页面打开后,在下拉菜单中展示默认第一个省、第一个城市、第一个地区。

为什么需要:这个需求不是一次请求就能完成。省份列表拿到后,要选第一个省;再拿着省名去请求城市;城市拿到后,再拿着省名和城市名去请求地区。后一次请求依赖前一次请求的结果,所以代码位置必须一层层往里写。
怎么想:先请求省份,在 then 回调里渲染第一个省;再在同一个回调里请求城市,在城市请求的 then 里渲染第一个城市;最后在城市回调里请求地区。也就是说,回调函数里又调用回调函数,一直嵌套下去。
怎么做:按“获取数据 → 取第一项 → 渲染到页面”的步骤写。注意 pname、cname 来自上一层作用域,所以内层请求能直接使用。
易错点:不要在没有拿到省名时就去请求城市;渲染时取的是 list[0],不是整个数组;请求参数名要和接口文档一致。
概念怎么想:回调函数中再调用回调函数
回调函数地狱指在回调函数中嵌套回调函数,一直嵌套下去形成层层缩进的结构。它的缺点有三个:可读性差、异常无法捕获、耦合性严重,牵一发动全身。
为什么需要理解概念:实际开发中使用回调函数时,可能写出这种结构。先认识它,后面才能用 Promise 链式调用、async/await 去改写。
怎么想:看代码不要只看“能不能跑”,还要看结构。第一次请求的结果在第一个 then 里接收;第二次请求写在第一个 then 内部;第三次请求又写在第二个 then 内部。层级越深,阅读和修改越困难。
怎么做:下面代码保留了本节示例,并故意把地区地址改错,用于测试异常捕获:
javascript
// 1. 获取省份数据并展示第1个省
axios({
url: 'http://hmajax.itheima.net/api/province'
})
.then(res => {
// console.log(res)
const pname = res.data.list[0]
document.querySelector('.province').innerText = pname
// 2. 获取第1个省的城市数据,并展示第1个城市
axios({
url: 'http://hmajax.itheima.net/api/city',
params: {
pname
}
})
.then(res => {
// console.log(res)
const cname = res.data.list[0]
document.querySelector('.city').innerText = cname
// 3. 获取第1个城市的区数据,并展示第1个区
axios({
// url: 'http://hmajax.itheima.net/api/area',
// 故意改错的地址,测试用
url: 'http://hmajax.itheima.net/api/area123',
params: {
pname, cname
}
})
.then(res => {
// console.log(res)
const aname = res.data.list[0]
document.querySelector('.area').innerText = aname
}).catch(err => {
console.dir(err)
})
})
}).catch(err => {
console.dir(err)
})易错点:嵌套层级深时,括号和 .catch 很容易放错位置;不要以为最外层写一个 .catch 就能捕获所有内层异常。
调试过程:异常捕获为什么困难
为什么需要调试:只看概念不容易感受到缺点。把地区请求地址改成错误地址后,控制台会出现异常。此时如果只在外层 then 后面写 .catch,内层请求的错误并不能被最外层捕获。转写稿中的调试结论是:要捕获最里面请求的异常,需要在对应内层 then 后面再写 .catch。
怎么想:每个 then 管理自己这一段异步任务,错误也应贴着对应的 then 处理。三个请求如果都要捕获异常,就要写多个 .catch,维护起来麻烦,这就是“异常捕获困难”。
怎么做:测试时先把地址改错,观察控制台;确认问题后,在内层 then 后补 .catch,用 console.dir(err) 查看错误对象;测试完再把地址恢复正确。
易错点:这里的“异常无法捕获”不是绝对无法捕获,而是最外层捕获不到内层异常,需要逐层处理;改错地址只用于测试,不能留在最终代码中。
小结与易错点
回调函数地狱的结构是:回调函数中嵌套回调函数。主要缺点是可读性差、异常捕获困难、耦合性严重。省市区案例中,城市请求依赖省名,地区请求依赖省名和城市名,所以代码只能层层写进回调里。写的时候要特别注意:请求位置必须在拿到上一层数据之后;pname、cname 的作用域要能覆盖内层;.catch 要放在需要捕获异常的那一层;调试改错地址后要恢复。理解这些,才能体会后续 Promise 链式调用和 async/await 改写代码的价值。
为什么需要链式调用
回调函数地狱的典型结构是“回调函数中再调用回调函数”:第一次请求的结果在第一个 then 回调里接收,第二次请求又写在这个回调内部,第三次再往里嵌套。转写稿里用省市区案例演示时,代码会不断换行、缩进,可读性变差;异常捕获也麻烦,内层请求出错时,外层一个 catch 往往接不住,需要每一层分别处理。

链式调用的目标不是消灭回调,而是把“层层嵌套”改成“平级串联”:上一个任务结束后,返回下一个异步任务,再在后面继续 .then() 接收结果。
核心机制:then 返回一个新 Promise
为什么需要:如果 then() 不返回新 Promise,就无法继续串联下一环任务。
怎么想:把 then() 看成“登记上一环任务完成后的处理函数”,它执行完会生成一个新的 Promise。这个新 Promise 管什么任务,由 then() 回调函数的返回值决定。
怎么做:转写稿中先验证 then() 的返回值:
js
const p = getPName()
const p2 = p.then(res => {
console.log(res) // 省份数据
})
console.log(p2 instanceof Promise) // true
console.log(p === p2) // false这说明 p2 是新生成的 Promise,不是原来的 p。接着在回调中 return 下一个异步任务:
js
function getPName() {
return new Promise(resolve => {
setTimeout(() => resolve('省份数据'), 2000)
})
}
function getCName() {
return new Promise(resolve => {
setTimeout(() => resolve('城市数据'), 2000)
})
}
getPName()
.then(res => {
console.log('第一次异步任务结果:', res)
return getCName() // 返回下一个异步任务
})
.then(res => {
console.log('第二次异步任务结果:', res)
})易错点:then() 回调里只是调用异步函数、却没有 return,下一个 then() 就不会等待它。比如只写 getCName(),第二个 then() 拿到的是 undefined,而不是城市数据。另一个易错点是误以为 then() 返回原 Promise;转写稿用 p === p2 打印出 false,就是为了纠正这一点。
怎么写:把省市区嵌套改成链式
为什么需要:省市区请求有先后依赖,必须拿到省份才能查城市,拿到城市才能查地区。
怎么想:每一步 then() 中做两件事:使用当前结果、return 下一个请求。最后一个 then() 只处理最终结果。
怎么做:结合转写稿的 axios 案例,结构可写成:
js
let pName // 第一个 then 中赋值,后面还要用
axios.get('省份列表接口').then(res => {
pName = res.data.list[0]
// 渲染省份
return axios.get('城市列表接口', { params: { pName } })
}).then(res => {
const cName = res.data.list[0]
// 渲染城市
return axios.get('地区列表接口', { params: { pName, cName } })
}).then(res => {
const areaName = res.data.list[0]
// 渲染地区
})转写稿调试时发现:pName 如果在第一个 then() 里用 const 声明,后面第二个、第三个 then() 拿不到它,因为链式调用中的回调并不是嵌套作用域。解决方式是把 pName 提到外层声明,第一个 then() 中只赋值。这个细节很容易错。
易错点:忘记 return axios.get(...);把 params 写成 perms;把 pName 声明在错误作用域;在 then() 里写渲染代码后忘了返回下一个请求。
调试与执行顺序
转写稿中的验证顺序是:先打印省份数据,再打印城市数据。若在第一个 then() 中 return getCName(),第二个 then() 才会等城市请求完成;若没有 return,第二个 then() 不会等待。链式调用后代码是平级的,没有层层嵌套,可读性比回调函数地狱更高。
小结
什么是 Promise 链式调用?
利用then()返回新 Promise 的特性,继续串联下一环任务,直到结束。then()回调中return的值传给哪里?
传给then()生成的新 Promise 对象;如果返回的是 Promise,下一个then()会等待它。链式调用有什么用?
解决回调函数嵌套问题,把嵌套写法改成链式写法。
从回调地狱到链式调用:为什么需要改写
为什么需要:回调函数里再调用回调函数,会形成层层嵌套的“回调地狱”。转写稿里的省市区案例就是典型:先请求省份,在它的回调里请求城市,再在城市回调里请求地区。逻辑一多,换行和缩进越来越深,可读性差,异常捕获也麻烦。
怎么想:不要继续往回调里塞回调,而是把每个异步任务都交给一个 Promise 管理,再用 then 把任务串起来。这样代码从“嵌套结构”变成“平级链条”。
怎么做:先拿到省份 Promise,在 then 中处理省份数据,并 return 获取城市的 Promise;下一个 then 再处理城市数据,并 return 获取地区的 Promise。核心是“上一个 then 返回下一个异步任务”。

易错点:不要把三个请求写成并列调用,因为它们有数据依赖;也不要在 then 里只调用请求却忘记 return,否则下一个 then 接不到这个 Promise。
then 为什么能链式调用:返回值决定下一个 Promise
为什么需要:只有理解 then 的返回值,才知道链式调用为什么能成立。转写稿明确:then 方法会返回一个新生成的 Promise 对象,所以后面可以继续点 then。
怎么想:then 回调里 return 什么,就会影响新 Promise 的状态和结果。若 return 一个 Promise,下一个 then 就会等待它成功并拿到结果。
怎么做:可以用两个模拟异步函数验证:
js
function getPName() {
return new Promise((resolve) => {
setTimeout(() => resolve('省份数据'), 2000)
})
}
function getCName() {
return new Promise((resolve) => {
setTimeout(() => resolve('城市数据'), 2000)
})
}
getPName()
.then((res) => {
console.log(res) // 省份数据
return getCName() // 返回下一个异步任务
})
.then((res) => {
console.log(res) // 城市数据
})易错点:忘记 return getCName(),下一个 then 的 res 就是 undefined;then 返回的是新 Promise,不是原来那个 Promise,转写稿中用 P === P2 测试得到 false。
用链式调用改写省市区案例:怎么做
为什么需要:实际案例中省、市、区必须按顺序获取,前一个结果决定后一个请求参数。用链式调用既能保留顺序,又能去掉嵌套。
怎么想:页面打开先请求省份;第一个 then 中保存并渲染省份,然后 return 城市请求;第二个 then 中保存并渲染城市,再 return 地区请求;第三个 then 中渲染地区。
怎么做:核心结构如下,接口地址按实际文档填写:
js
let pName // 转写稿中改为全局变量,方便后续使用
axios({ url: '省份接口' })
.then((res) => {
pName = res.data.list[0]
// 渲染省份
return axios({ url: '城市接口', params: { pName } })
})
.then((res) => {
const cName = res.data.list[0]
// 渲染城市
return axios({ url: '地区接口', params: { pName, cName } })
})
.then((res) => {
const aName = res.data.list[0]
// 渲染地区
})易错点:第三个请求需要 pName 时,若 pName 声明在第一个 then 内部,后面的平级 then 访问不到。转写稿的调试过程就是:控制台报 pName is not defined,于是把 const pName 改成全局 let pName,并在第一个 then 中赋值,问题解决。
调试过程:从报错看链式调用的关键点
为什么需要:链式调用写错时,报错往往指向作用域或返回值,需要按控制台提示定位。
怎么想:先看请求是否按顺序发出,再看每个 then 是否 return 了下一个 Promise,最后检查变量作用域是否覆盖到使用位置。
怎么做:转写稿中先完成省份渲染;再把城市请求写在第一个 then 内并 return 出去,后面接第二个 then;地区请求同理写在第二个 then 内并 return,最后接第三个 then。保存后依次看到省、市、区数据渲染。
易错点:链式调用不是“把代码写在同一行”就算完成,关键是每个 then 的返回值要正确;另外,回调地狱原本异常捕获困难,链式调用中也要注意在合适位置处理失败,不能只写成功分支。
小结
- Promise 链式调用如何解决回调函数地狱?
答案
- then 的回调函数中 return Promise 对象,影响当前新 Promise 对象的值
从回调地狱到 async/await 的动机
为什么需要:回调函数里再调用回调函数,会形成“回调函数地狱”。转写稿里用“获取省、市、区”的案例演示:第一次请求的结果在第一个 then 回调里接收,第二次请求写在它内部,第三次再往里嵌套。这样虽然能拿到数据,但层层嵌套、换行缩进多,可读性差;异常捕获也麻烦,外层一个 .catch 捕获不到内层错误,往往要在每个 then 后分别写 .catch。
怎么想:Promise 链式调用已经把“嵌套”改成“链式”,在 then 回调中 return 下一个 Promise 对象,就可以继续 .then。但链式写法仍有链式结构,变量作用域也不如平铺代码直观。async 和 await 的目标,是让我们在 async 函数内用近似同步代码的方式等待 Promise 成功结果,不再刻意写 then 链。
怎么做:本节笔记给出的核心结构是:
javascript
async function func() {
const res1 = await Promise对象1
const res2 = await Promise对象2
const res3 = await Promise对象3
}
func()易错点:await 不是“让整个程序停住”,而是暂停当前 async 函数内部后续代码,等后面的 Promise 成功后再把结果赋给左侧变量;外部其他代码仍可继续执行。不要把它理解成阻塞页面。
async 函数和 await 的基本语法
为什么需要:如果只用 then,每次取结果都要写回调函数;多个异步任务连续执行时,代码结构不够平直。async/await 让“等待结果”这一步更简洁。
怎么想:先声明一个 async 函数,在函数体内部写 await Promise对象。代码执行到 await 时,会等待后面的 Promise 成功,并把成功结果赋值给左侧变量;普通同步代码照常从上往下写。转写稿中演示时,先写 async function func(),再在内部用 await axios(...) 之类的请求结果,随后直接 console.log 或渲染页面。
怎么做:按笔记结构,把原来的 then 改成 await。例如连续三个异步任务可以平铺为 res1、res2、res3,后面直接使用这些变量。转写稿的省市区案例最终就是:先 await 省份请求,取第一个省并渲染;再 await 城市请求,取第一个城市并渲染;再 await 区请求,取第一个区并渲染。代码没有回调嵌套,也没有链式 .then。
易错点:第一,await 要写在 async 函数内,按本节语法不要在普通函数里直接使用。第二,忘记写 await 时,拿到的是 Promise 对象,不是成功结果。转写稿专门删掉 await 测试,打印出来是 Promise 对象,后续再取属性就会报错。第三,await 后面通常等待 Promise;普通同步代码不需要 await。
用 await 替代 then 提取成功结果
为什么需要:then 的作用是注册回调,从 Promise 成功状态中取结果;await 可以直接把成功结果赋给变量,减少回调层级。
怎么想:把“Promise对象.then(res => { ... })”理解为“等待这个 Promise 成功,然后把结果交给 res”。用 await 后,就是把结果直接赋给左侧变量,后续逻辑继续平铺。
怎么做:笔记中的写法是:
javascript
async function func() {
const res1 = await Promise对象1
const res2 = await Promise对象2
const res3 = await Promise对象3
}
func()这段代码表达的是:res1 拿到第一个 Promise 成功结果后,再执行第二个 await;res2 拿到后再执行第三个。转写稿调试时也看到,加上 await 后能正常打印省份、城市、地区数据;不加 await 则拿到 Promise 对象。
易错点:await 只负责等待成功结果;失败处理不能靠它“自动解决”。本节笔记的小结也强调:await 的作用是替代 then 方法来提取 Promise 对象成功状态的结果。失败场景要用下一部分的 try/catch。
用 try/catch 捕获 async 函数中的错误
为什么需要:await 等待 Promise 时,如果请求失败,错误需要被捕获,否则会中断当前 async 函数后续代码。转写稿中把第二个请求 URL 后面故意加 123,控制台出现 404,message 为 not find,说明请求失败。
怎么想:async 函数内部可以使用普通的 try...catch。把需要执行的异步代码放进 try,出错后进入 catch,通过 catch (err) 拿到错误信息。
怎么做:转写稿的结构是:try 中放原来的请求和渲染逻辑;catch 中打印 err。调试时先不改错,页面数据正常渲染,catch 不执行;再把 URL 改错,catch 执行并打印错误。错误发生后,try 中后续代码不再继续,所以页面上只渲染了第一份数据,第二、第三份没有渲染。
易错点:try 里没有错误时,catch 不会执行;错误一旦发生,try 内后续语句会中断。不要以为 catch 会“修复”请求,它只是接收并处理错误信息。
小结
为什么需要:记住 await 的定位,避免把它当成普通函数或阻塞语句。怎么想:async 声明函数,await 等待 Promise 成功结果。怎么做:在 async 函数内用 await 取代 then 提取结果;失败用 try/catch。易错点:不加 await 得到 Promise 对象;错误会中断后续代码。
await 的作用是什么?
替代 then 方法来提取 Promise 对象成功状态的结果。
为什么需要 try catch 捕获错误
写代码时,错误并不总是“语法错误”。更多时候是运行过程中才出现的问题,例如请求地址写错、服务器返回 404、某个数据取不到。如果不管它,代码会中断,控制台会报错,页面后续渲染也可能停掉。
try...catch 的作用就是:把可能出错的代码放进 try 里,出错后不要让程序直接“炸掉”,而是立刻跳到 catch 中处理错误。本节笔记的目标是了解用 try catch 捕获同步流程的错误;放到 async 函数里,就是用它包住 await 相关代码,捕获异步请求失败等错误。
怎么想:先明确分工——try 放“要执行的语句”,catch 放“出错后要执行的语句”。catch 的参数 error 用来接收错误信息。
怎么做:保留本节笔记中的基本结构:
javascript
try {
// 需要被执行的语句
} catch (error) {
// error 接收错误信息
// try 有错误时执行的语句
}易错点:try 里没有错误时,catch 不会执行;不要以为写了 catch 就一定会打印。
先想清楚:try 里报错后会立刻跳转
转写稿里反复强调一句话:try 里报错的代码会立刻跳转到 catch 中。也就是说,try 中一旦某一行出错,它后面的代码就不再继续执行,而是直接把错误信息带到 catch 的 error 参数里。
为什么需要:理解这个跳转过程,才能解释“为什么页面只渲染了第一份数据,后面的数据没有渲染”。在调试中,第二个请求地址被故意改错后,try 中后续代码中断,所以第二、第三份数据没有继续处理。
怎么想:把 try 看成一个“受保护区域”。区域里正常就顺序执行;区域里出错就立即退出,进入 catch。
怎么做:在 catch 中先打印错误,确认是否捕获到:
javascript
try {
// 需要被执行的语句
} catch (error) {
console.dir(error)
}易错点:不要认为 try 里出错后还会继续执行下一行;实际是中断并跳转。
在 async 函数里怎么写 try catch
转写稿说明:使用 async 关键字声明函数后,就可以在异步代码中使用普通的 try catch 代码块。写法不是新的语法,仍然是普通 try...catch,只是放在 async 函数内部,包住 await 的代码。
为什么需要:await 等待的 Promise 如果失败,需要有地方接收错误;try...catch 就是统一处理入口。
怎么想:函数用 async 声明;需要执行的请求逻辑放进 try;出错处理放进 catch。例如结构可以写成:
javascript
async function getData() {
try {
const res = await axios.get('/接口地址')
console.log(res)
} catch (error) {
console.dir(error)
}
}怎么做:先把原来的请求逻辑剪切到 try 里,再在 catch 中通过 error 获取错误信息。转写稿中的调试就是:结构改完后,页面数据仍能正常渲染,因为 try 里没有出错,catch 不执行;随后把第二次请求的 URL 后面随便加上 123 制造错误,控制台出现错误输出,说明 catch 执行了。
易错点:async 函数里仍然写的是 try catch,不是别的关键字;catch 的参数名可以叫 error,也可以叫 err,关键是它接收错误信息。
调试过程:看 catch 是否执行、错误信息是什么
转写稿的调试过程很值得还原:先只改结构、不改错,确认页面正常,控制台没有 catch 输出;再把第二个请求地址改错,保存后到控制台观察。结果出现错误打印,并且对应行号指向 catch 里的输出,说明 try 中出错后确实进入了 catch。
接着查看错误对象:服务器返回的信息在 response.data 里,message 的值是 not find,表示找不到;状态码 status 是 404,也表示资源找不到。这两个信息是对应的。
为什么需要:捕获错误不只是“知道错了”,还要读出错误原因,方便定位是地址错、参数错还是服务器问题。
怎么做:在 catch 中先用 console.dir(error) 或 console.log(error) 输出,再展开 response、data、message、status 查看。
易错点:不要只看“报错了”三个字;要确认错误对象里的 message 和 status 是否一致。转写稿中 not find 与 404 对上,才说明判断正确。
易错点汇总
第一,try 中没有错误时,catch 不会执行,所以控制台没有输出是正常的。第二,try 中一旦出错,后面的代码不再执行,页面只完成出错前的渲染。第三,catch 的参数 error 是错误信息入口,不要写成没有参数的 catch 后还奇怪为什么拿不到信息。第四,async 函数中如果不写 await,拿到的是 Promise 对象而不是成功结果;本节虽然重点讲错误捕获,但调试时也要注意 await 是否写对。第五,本节笔记的小结是“捕获同步流程的代码报错信息”,而在 async 函数中,try catch 可以包住 await 代码,把等待过程中的失败交给 catch 处理。掌握这套结构后,请求出错时就能在 catch 中统一接收并查看错误信息,而不是让代码直接中断。
课件图示








代码示例
示例一:同步代码与异步代码的执行顺序
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>同步代码与异步代码</title>
</head>
<body>
<button id="btn">点我</button>
<script>
/* 1. 同步代码:逐行执行,原地等待结果 */
console.log(1)
const num = 1 + 1
console.log(num) // 2
/* 2. 同步 + 异步(定时器):先打印 1、3,1 秒后打印 2 */
console.log(1)
setTimeout(() => {
console.log(2)
}, 1000)
console.log(3)
/* 3. 同步 + 异步(事件):先打印 1、3,点击后才打印 2 */
console.log(1)
document.body.addEventListener('click', () => {
console.log(2)
})
console.log(3)
/* 4. 同步 + 异步(XHR):先打印 1、3,服务器响应回来后才打印 2 */
console.log(1)
const xhr = new XMLHttpRequest()
xhr.open('get', 'http://hmajax.itheima.net/api/province')
xhr.addEventListener('loadend', () => {
console.log(2)
})
xhr.send()
console.log(3)
</script>
</body>
</html>要点解释:
setTimeout(() => {...}, 1000)只是“登记”了一个 1 秒后的任务,代码不会停在原地等,而是继续往下走,所以先输出3,1 秒后才输出2。addEventListener('click', ...)只是注册事件,回调函数要满足触发条件(点击)才会执行,不点就永远不打印2;注册事件同样属于异步代码。xhr.send()把请求发给服务器后立刻返回,后面的console.log(3)马上执行;loadend回调要等服务器响应回来才触发,此时才打印2。- 易错点:把“异步”理解成“慢”,以为定时器或请求会阻塞后面的代码;实际上异步代码不会阻塞后续执行,结果统一靠回调函数接收。
示例二:Promise 链式调用
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Promise 链式调用</title>
</head>
<body>
<script>
/* 用定时器模拟一个耗时 2 秒的异步任务,返回 Promise 对象 */
function getPName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('省份数据')
}, 2000)
})
}
function getCName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('城市数据')
}, 2000)
})
}
/* then 返回的是一个“新”的 Promise 对象,可以继续 .then 串下去 */
const p = getPName()
const p2 = p.then(res => {
console.log(res) // 省份数据
return getCName() // 返回值决定新 Promise 管理的异步任务
})
console.log(p === p2) // false,p2 是新生成的 Promise 对象
/* 链式调用写法:没有回调函数嵌套 */
getPName()
.then(res => {
console.log(res) // 第一次异步任务的结果:省份数据
return getCName() // 返回下一个异步任务
})
.then(res => {
console.log(res) // 第二次异步任务的结果:城市数据
})
</script>
</body>
</html>要点解释:
.then()方法会返回一个新生成的 Promise 对象,所以可以在后面继续.then(),这就是链式调用的根本原因。.then()回调函数里return的值,会传给新生成的那个 Promise 对象:return getCName()就是让新 Promise 去管理“获取城市数据”这个异步任务,于是下一个.then()的res拿到的就是城市数据。- 不
return下一个 Promise,下一个.then()就接不到任何有用结果——这是最常见的漏写点。 p === p2打印false,说明新对象不是原来那个 Promise;链式调用解决的是回调函数嵌套,代码是平级的,可读性更好。
示例三:async / await + try catch 获取省市区
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>async 和 await 解决回调地狱</title>
</head>
<body>
<div class="province"></div>
<div class="city"></div>
<div class="area"></div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
async function func() {
try {
// 1. 获取省份数据并展示第 1 个省
const res1 = await axios({
url: 'http://hmajax.itheima.net/api/province'
})
const pname = res1.data.list[0]
document.querySelector('.province').innerText = pname
// 2. 获取第 1 个省的城市数据,展示第 1 个城市
const res2 = await axios({
url: 'http://hmajax.itheima.net/api/city',
params: { pname }
})
const cname = res2.data.list[0]
document.querySelector('.city').innerText = cname
// 3. 获取第 1 个城市的区数据,展示第 1 个区
const res3 = await axios({
url: 'http://hmajax.itheima.net/api/area',
params: { pname, cname }
})
const aname = res3.data.list[0]
document.querySelector('.area').innerText = aname
} catch (err) {
// try 里报错会立刻跳到 catch,err 就是错误信息
console.dir(err)
}
}
func()
</script>
</body>
</html>要点解释:
await只能写在async函数内部,它替代了.then():等待后面的 Promise 对象成功,并把成功结果赋值给左侧变量,所以res1直接就是 axios 的响应对象,可以res1.data.list[0]取值。- 易错点:漏写
await时,res3拿到的是一个 Promise 对象,res3.data是undefined,会直接报错——这是本节最典型的错误。 try { ... } catch (err) { ... }捕获的是这段同步流程里的错误:try中某一行报错(例如接口地址写错),后面的代码立刻不再执行,直接跳进catch,err里就是错误信息。pname、cname在同一个函数作用域里声明,后面直接就能用,不需要像链式调用那样靠return传值,也不用改成全局变量。- 页面里用类名相同的
div承载文本(笔记演示时是给下拉框的 option 赋值),核心逻辑完全一致:innerText赋值即可,不用拼标签。
常见错误
1. await 写在普通函数里,控制台直接报语法错误
- 现象:
Uncaught SyntaxError: await is only valid in async functions,整段代码一行都不执行。 - 原因:
await只能在async修饰的函数体内出现,写在外层普通函数或全局顶层(旧环境)都会解析失败。 - 修法:给最外层函数加上
async;如果不想改结构,就退回.then()写法。注意async加在函数声明上,不是加在await那一行。
2. await 漏写,变量里拿到的是 Promise 对象
- 现象:
res.data报Cannot read properties of undefined,或者打印出来是Promise {<pending>}。 - 原因:
axios(...)本身返回 Promise,不写await时赋值给变量的就是 Promise 本身,不是响应结果。 - 修法:
const res = await axios(...),每个取结果的调用点都要检查是否带await;调试时先console.log(res)确认它是 Promise 还是数据。
3. 链式调用中第二个 then 拿不到上一个的结果
- 现象:第二个
.then(res => ...)里的res是undefined,或者直接跳过。 - 原因:上一个
then的回调用了花括号却没return下一个 Promise(axios(...)前面少写return),新 Promise 就变成 fulfilled 值为undefined。 - 修法:回调体用简写形式直接返回,或显式写
return axios(...);改完看缩进——.then要和上一个.then平级,不是缩进到里面。
4. 多个 then 里复用变量报 is not defined
- 现象:回调地狱改链式调用后,第三个
then里用pname报未定义;改async/await后又莫名其妙好了。 - 原因:链式调用中每个
then回调是独立作用域,是平级的,后面的then访问不到前面then里const声明的变量。 - 修法:要么在最外层用
let声明变量(链式调用场景),要么改用async/await—— 所有await都在同一个函数体内,变量天然共享。
5. 用 try/catch 包 axios().then() 却捕不到错误
- 现象:接口地址故意写错,
catch块没反应,控制台反而是未捕获的 rejection。 - 原因:
try/catch只能捕获同步流程抛出的错误;.then()里的失败是异步 reject,早已跳出try的执行栈。 - 修法:把 Promise 用
await摊平成同步写法再包try,即try { const res = await axios(...) } catch(err) {...};坚持用then就补.catch()。
6. try 范围包小了,出错后页面只渲染了一半
- 现象:三个下拉框只出了第一个,后面两个空白,
catch里也没打印。 - 原因:
try只包住了第一个await,后面两个请求在try之外,出错后既不渲染也没人捕获;或者反过来,try包住了整段但catch里只写了空语句。 - 修法:把整条
await业务链一起放进try,catch里必须console.dir(err)+ 给用户提示;改完故意把某个 URL 改错一次验证catch真的会走。
练习
1. 写出打印顺序
javascript
console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))
console.log('D')说明为什么 B 和 C 的顺序是这样(提示:一个进宏任务队列,一个进微任务队列)。
2. 改写回调地狱 把下面这段改写成 async/await 版本,要求不出现任何嵌套 then:
javascript
axios({url:'/api/province'}).then(res => {
const pname = res.data.list[0]
axios({url:'/api/city', params:{pname}}).then(res2 => {
const cname = res2.data.list[0]
axios({url:'/api/area', params:{pname, cname}}).then(res3 => {
render(res3.data.list[0])
})
})
})3. 找 bug
javascript
async function getData() {
const res = axios({url:'/api/province'})
document.querySelector('.province').innerText = res.data.list[0]
}
getData()说出至少两处问题并改正。
4. 找 bug(链式调用)
javascript
axios({url:'/api/province'})
.then(res => {
const pname = res.data.list[0]
axios({url:'/api/city', params:{pname}})
})
.then(res => {
console.log(res.data.list[0])
})说明第二个 then 会打印什么,并给出两种修法。
5. 用 Promise 包装定时器 实现 delay(ms),使其可以被 await:await delay(1000); console.log('1秒后')。要求不用 setTimeout 直接返回、不能返回 undefined。
6. 错误处理补全 给练习 2 的 async/await 版本加上错误处理,要求:请求失败时 catch 能拿到错误对象,并且控制台打印出服务器返回的 message 与状态码。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 同步代码和异步代码 | 09:14 |
| 2 | 回调函数地狱 | 14:01 |
| 3 | Promise-链式调用 | 11:01 |
| 4 | Promise-链式调用-解决回调地狱 | 11:34 |
| 5 | async函数和await | 11:27 |
| 6 | async函数和await-错误捕获 | 04:59 |
小结
- 判断一段代码会不会"插队",只看它是不是回调函数 / Promise 回调形式——是就排到后面去,和它写在第几行无关。
- 回调地狱的本质不是"回调太多",而是异步结果只能出现在回调里,导致后续依赖只能往里缩进;所以解法的核心是"把结果提到同一层",嵌套和链式都只是手段。
- 链式调用的成败取决于一个
return:上一个then返回什么,下一个then就收到什么,忘了return整条链就断在半路。 async/await之所以是终极方案,是因为它把变量拉回了同一个函数作用域,链式调用中最容易踩的跨then取变量问题直接消失。await只是"等结果",不是"吞错误"——不加try/catch,失败时依然是未捕获的 rejection,页面会静默地停在半截。try/catch只对同步流程有效,遇到 Promise 必须先await摊平,这条规律同样适用于任何"包了却不生效"的排查。- 三种写法都要会:真实项目里
.then()和async/await常常混用,读得懂旧代码、写得清新代码,才算真正掌握。 - 调试异步问题的第一动作永远是
console.log打出那个变量——先确认它是数据还是 Promise,能省掉一半排查时间。