深入理解JavaScript异步编程:从回调地狱到Promise/Async优雅解决方案
一、同步代码和异步代码
1.同步代码的定义
逐行执行,原地等待结果后才继续向下执行
浏览器按照代码书写顺序逐行解析和执行,上一行代码执行完成后才会执行下一行,后续代码依赖前一行的执行结果。这种执行方式具有顺序性和阻塞性,即必须等待当前代码执行完毕才能继续后续操作。
- 特点:顺序执行、阻塞主线程、后续代码依赖前一行结果。
- 示例:普通赋值语句(
const result = 0 + 1
)、函数调用等。
2.异步代码的定义
调用后耗时,不阻塞代码继续执行,在将来完成后触发一个回调函数
异步代码在调用后不会阻塞主线程,而是将耗时任务交给宿主环境(如浏览器)处理,主线程继续执行后续代码。当异步任务完成时,通过回调函数处理结果。
- 特点:非阻塞性、通过回调函数处理结果、不影响主线程后续代码执行。
- 常见场景:
setTimeout
/setInterval
、事件监听(如点击事件)、AJAX 请求等。
3.核心区别
特性 | 同步代码 | 异步代码 |
---|---|---|
执行方式 | 逐行顺序执行,阻塞主线程 | 非阻塞,异步任务后台处理 |
结果处理 | 直接获取结果 | 通过回调函数处理结果 |
典型场景 | 普通赋值、函数调用 | 定时器、事件、AJAX 请求 |
二、回调函数地狱和Promise 链式调用
1.回调函数地狱
需求:展示默认第一个省,第一个城市,第一个地区在下拉菜单中
概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱
缺点:可读性差,异常无法捕获,耦合性严重,牵一发动全身
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>回调地狱</title>
</head><body><form><span>省份:</span><select><option class="province"></option></select><span>城市:</span><select><option class="city"></option></select><span>地区:</span><select><option class="area"></option></select></form><script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script><script>/*** 目标:演示回调函数地狱* 需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中* 概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱* 缺点:可读性差,异常无法获取,耦合性严重,牵一发动全身*/// 1. 获取默认第一个省份的名字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)})</script>
</body></html>
2.Promise - 链式调用
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Promise_链式调用</title>
</head><body><script>/*** 目标:掌握Promise的链式调用* 需求:把省市的嵌套结构,改成链式调用的线性结构*/// 1. 创建Promise对象-模拟请求省份名字const p = new Promise((resolve, reject) => {setTimeout(() => {resolve('北京市')}, 2000)})// 2. 获取省份名字const p2 = p.then(result => {console.log(result)// 3. 创建Promise对象-模拟请求城市名字// return Promise对象最终状态和结果,影响到新的Promise对象return new Promise((resolve, reject) => {setTimeout(() => {resolve(result + '--- 北京')}, 2000)})})// 4. 获取城市名字p2.then(result => {console.log(result)})// then()原地的结果是一个新的Promise对象console.log(p2 === p)</script>
</body></html>
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Promise链式调用_解决回调地狱</title>
</head><body><form><span>省份:</span><select><option class="province"></option></select><span>城市:</span><select><option class="city"></option></select><span>地区:</span><select><option class="area"></option></select></form><script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script><script>/*** 目标:把回调函数嵌套代码,改成Promise链式调用结构* 需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中*/let pname = ''// 1. 得到-获取省份Promise对象axios({url: 'http://hmajax.itheima.net/api/province'}).then(result => {pname = result.data.list[0]document.querySelector('.province').innerHTML = 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').innerHTML = cname// 3. 得到-获取地区Promise对象return 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})</script>
</body></html>
三、async 和await 使用
1.async
函数的定义
使用 async
关键字声明的函数,返回一个 Promise
,并允许在其内部使用 await
关键字
async
函数会隐式地将返回值包装为Promise
,即使没有显式返回Promise
,也会返回Promise.resolve(返回值)
。- 作用:简化异步代码的书写,使异步逻辑更接近同步代码的写法,避免回调地狱。
- 语法:
async function 函数名([参数]) {// 异步操作或同步逻辑return 结果; // 隐式包装为 Promise }
2.await
关键字的定义
用于等待一个 Promise
解决(resolve
),并获取其结果值,只能在 async
函数内部使用
- 当
await
后面的Promise
未解决时,会暂停async
函数的执行,直到Promise
状态变为fulfilled
,并返回其结果值。 - 若
await
后面是普通值(非Promise
),会直接返回该值(等效于Promise.resolve(普通值)
)。 - 作用:以同步代码的写法处理异步逻辑,使异步操作更直观。
- 语法:
const 结果 = await Promise或普通值;
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>async函数和await_解决回调函数地狱</title>
</head><body><form><span>省份:</span><select><option class="province"></option></select><span>城市:</span><select><option class="city"></option></select><span>地区:</span><select><option class="area"></option></select></form><script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script><script>/*** 目标:掌握async和await语法,解决回调函数地狱* 概念:在async函数内,使用await关键字,获取Promise对象"成功状态"结果值* 注意:await必须用在async修饰的函数内(await会阻止"异步函数内"代码继续执行,原地等待结果)*/// 1. 定义async修饰函数async function getData() {// 2. await等待Promise对象成功的结果const pObj = await axios({url: 'http://hmajax.itheima.net/api/province'})const pname = pObj.data.list[0]const cObj = await axios({url: 'http://hmajax.itheima.net/api/city', params: { pname }})const cname = cObj.data.list[0]const aObj = await axios({url: 'http://hmajax.itheima.net/api/area', params: { pname, cname }})const areaName = aObj.data.list[0]document.querySelector('.province').innerHTML = pnamedocument.querySelector('.city').innerHTML = cnamedocument.querySelector('.area').innerHTML = areaName}getData()</script>
</body></html>
3.async函数和await_捕获错误
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>async函数和await_错误捕获</title>
</head><body><form><span>省份:</span><select><option class="province"></option></select><span>城市:</span><select><option class="city"></option></select><span>地区:</span><select><option class="area"></option></select></form><script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script><script>/*** 目标:async和await_错误捕获*/async function getData() {// 1. try包裹可能产生错误的代码try {const pObj = await axios({ url: 'http://hmajax.itheima.net/api/province' })const pname = pObj.data.list[0]const cObj = await axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })const cname = cObj.data.list[0]const aObj = await axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } })const areaName = aObj.data.list[0]document.querySelector('.province').innerHTML = pnamedocument.querySelector('.city').innerHTML = cnamedocument.querySelector('.area').innerHTML = areaName} catch (error) {// 2. 接着调用catch块,接收错误信息// 如果try里某行代码报错后,try中剩余的代码不会执行了console.dir(error)}}getData()</script>
</body></html>
四、事件循环-EventLoop
1.认识- 事件循环(EventLoop)
2.事件循环- 执行过程
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>事件循环_练习</title>
</head><body><script>/*** 目标:阅读并回答执行的顺序结果*/console.log(1)setTimeout(() => {console.log(2)}, 0)function myFn() {console.log(3)}function ajaxFn() {const xhr = new XMLHttpRequest()xhr.open('GET', 'http://hmajax.itheima.net/api/province')xhr.addEventListener('loadend', () => {console.log(4)})xhr.send()}for (let i = 0; i < 1; i++) {console.log(5)}ajaxFn()document.addEventListener('click', () => {console.log(6)})myFn()// 1 5 3 2 4 点击一次document就会执行一次打印6</script>
</body></html>
3.宏任务与微任务
4.宏任务与微任务- 执行顺序
5.事件循环- 经典面试题
console.log(1); // 1
setTimeout(() => { // 宏任务1(延迟0ms)console.log(2);const p = new Promise(resolve => resolve(3));p.then(result => console.log(result)); // 微任务3
}, 0);const p = new Promise(resolve => { // 同步执行Promise构造函数setTimeout(() => { // 宏任务2(延迟0ms)console.log(4);}, 0);resolve(5); // 触发微任务1
});
p.then(result => console.log(result)); // 微任务2const p2 = new Promise(resolve => resolve(6)); // 同步resolve
p2.then(result => console.log(result)); // 微任务4console.log(7); // 7
⑴.同步代码执行阶段
①console.log(1) 立即执行,输出 1。
②setTimeout(() => { console.log(2)... }, 0) 回调函数被放入宏任务队列(由于是 setTimeout)。
③const p = new Promise(...)
- 构造函数中的 setTimeout(() => { console.log(4) }, 0) 被放入宏任务队列。
- resolve(5) 立即执行,Promise p 变为 resolved 状态。
- p.then(...) 将回调 console.log(5) 放入微任务队列。
④ const p2 = new Promise(resolve => resolve(6)) resolve(6) 立即执行,Promise p2 变为 resolved 状态。 p2.then(...) 将回调 console.log(6) 放入微任务队列。
⑤console.log(7) 立即执行,输出 7。 此时同步代码执行完毕,输出顺序为:1 → 7。
⑵.微任务队列处理
执行 p.then(() => console.log(5))
输出 5。
执行 p2.then(() => console.log(6))
输出 6。
微任务队列清空,输出顺序更新为:1 → 7 → 5 → 6
。
⑶.宏任务队列处理
①执行第一个 setTimeout
的回调(对应 console.log(2)
):
- 输出 2。
new Promise(resolve => resolve(3))
立即执行,Promise 状态变为resolved
。p.then(() => console.log(3))
将回调console.log(3)
放入微任务队列。
②当前宏任务结束,立即处理微任务队列:
- 执行
console.log(3)
,输出 3。 - 执行第二个
setTimeout
的回调(对应console.log(4)
): - 输出 4。
最终输出顺序为:1 → 7 → 5 → 6 → 2 → 3 → 4
。
五、Promise.all 静态方法
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Promise的all方法</title>
</head><body><ul class="my-ul"></ul><script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script><script>/*** 目标:掌握Promise的all方法作用,和使用场景* 业务:当我需要同一时间显示多个请求的结果时,就要把多请求合并* 例如:默认显示"北京", "上海", "广州", "深圳"的天气在首页查看* code:* 北京-110100* 上海-310100* 广州-440100* 深圳-440300*/// 1. 请求城市天气,得到Promise对象const bjPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '110100' } })const shPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '310100' } })const gzPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '440100' } })const szPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '440300' } })// 2. 使用Promise.all,合并多个Promise对象const p = Promise.all([bjPromise, shPromise, gzPromise, szPromise])p.then(result => {// 注意:结果数组顺序和合并时顺序是一致console.log(result)const htmlStr = result.map(item => {return `<li>${item.data.data.area} --- ${item.data.data.weather}</li>`}).join('')document.querySelector('.my-ul').innerHTML = htmlStr}).catch(error => {console.dir(error)})</script>
</body></html>
六、案例- 商品分类
七、案例- 学习反馈
代码详见day04