同步和异步

  1. 同步代码:逐行执行,需原地等待结果后,才继续向下执行
  2. 异步代码:调用后耗时,不阻塞代码继续执行(不必原地等待),在将来完成后触发回调函数传递结果
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--这里用延时器模拟需要异步的操作-->
<button class="btn">按钮</button>
<script>
const result = 0 + 1
console.log(result)
setTimeout(() => {
console.log(2)
}, 2000)
document.querySelector('.btn').addEventListener('click', () => {
console.log(3)
})
console.log(4)
</script>
  • 结果就是先打印1、4,两秒之后打印2,最后点击按钮打印3

常见的异步操作

  1. 回调函数
  2. Promise
  3. async/await
  4. 事件监听
  5. 定时器
  6. XMLHttpRequestFetch API等网络请求
  7. Web Workers(Web Worker API提供了从主执行线程分离并在后台运行脚本的能力,即在后台运行JavaScript代码,不影响页面UI的渲染和响应能力)
  8. Node.js中的异步I/O操作(如读写文件、网络请求等)

回调函数地狱

概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
axios({ url: 'http://hmajax.itheima.net/api/province' }).then(result => {
const pname = result.data.list[0]
document.querySelector('.province').innerHTML = pname
// 获取第一个省份默认下属的第一个城市名字
axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } }).then(result => {
const cname = result.data.list[0]
document.querySelector('.city').innerHTML = cname
// 获取第一个城市默认下属第一个地区名字
axios({url: 'http://hmajax.itheima.net/api/area', params: {pname, cname}}).then(result => {
document.querySelector('.area').innerHTML = result.data.list[0]
})
})
})

以上代码是实现获取省份名称之后获取城市名称,再获取镇区名称的功能。出现了回调函数地狱问题。

虽然这样的代码可以正常运行,但存在以下问题:

  1. 可读性差:嵌套过多的回调函数使得代码难以阅读和理解,代码的意图变得模糊,不易于维护。
  2. 难以调试:回调函数地狱使得代码执行流程变得非常复杂,导致难以进行调试。当出现错误时,追踪问题和定位 bug 变得更加困难。
  3. 缺乏可扩展性:如果需要对现有代码进行修改或添加新功能,由于代码结构复杂,很容易引入更多的错误。而且,由于函数之间的紧密耦合,修改会牵一发而动全身
  4. 可维护性差:回调函数地狱中的代码难以维护和修改,因为任何一处的更改都可能会导致意想不到的后果。这增加了代码维护的成本和风险。
  • 这个时候就需要Promiseasync/await 等异步编程的改进方法来管理异步操作,使代码结构更清晰、可读性更强、易于维护和扩展。

Promise-链式调用

概念:依靠 then() 方法会返回一个新生成的 Promise 对象特性,继续串联下一环任务,直到结束

细节then() 回调函数中的返回值,会影响新生成的 Promise 对象最终状态和结果

好处:通过链式调用,解决回调函数嵌套问题

做法:每个 Promise 对象中管理一个异步任务,用 then 返回新的 Promise 对象,串联起来

image-20230222173851738.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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)

async和await

定义async函数是使用async关键字声明的函数。async函数是AsyncFunction构造函数的实例,并且其中允许使用await关键字。asyncawait关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调动Promise

概念:在async函数内,使用await关键字取代then函数,等待获取Promise对象成功状态的结果值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 别忘了引入axios.js文件
// 在函数前加async,在需要等待Promise对象结果的地方前加await,就会等待结果并赋值
async function getData() {
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]

console.log(pname)
console.log(cname)
console.log(areaName);
}
getData()

错误捕获

使用try...catch语句标记要尝试的语句块,并指定一个出现异常时抛出的响应

语法

1
2
3
4
5
6
try {
// 要执行的async函数
} catch (error) {
// error接收错误的信息
// 当try部分出现错误,则会在这里执行
}

事件循环(EventLoop)

概念:JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其他语言中的模型截然不同,比如C和Java。

原因:JavaScript是单线程,为了让耗时代码不阻塞其他代码运行,设计了事件循环模型

image-20230222182338992.png

事件循环机制

  1. 代码执行时,先执行同步任务,然后将异步任务放入任务队列中,等待执行。
  2. 当所有同步任务执行完毕后,JavaScript引擎会去读取任务队列中的任务。
  3. 将队列中的第一个任务压入执行栈中执行,执行完毕后将其出栈。
  4. 如此循环执行,直到任务队列中的所有任务都执行完毕。

用一个例子再次模拟一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log(1) // 同步任务,直接执行
setTimeout(() => {
console.log(2) // 延时器,属于异步任务,即使只是延时0ms
}, 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网络请求,属于异步任务
})
xhr.send()
}
for (let i = 0; i < 1; i++) {
console.log(5) // 同步任务,进入for循环直接执行
}
ajaxFn()
document.querySelector('.btn').addEventListener('click', () => {
console.log(6) // 事件监听,属于异步任务,需要同步任务执行完毕之后按下按钮才可触发
})
myFn()
  • 结果为1、5、3、2、4,按下按钮打印 6。

宏任务与微任务

宏任务(MacroTasks)和微任务(Micro Tasks)是指在JavaScript中异步任务队列中执行的不同类型任务。

ES6 之后引入了 Promise 对象, 让 JS 引擎也可以发起异步任务

异步任务划分为了

  • 宏任务:由浏览器环境执行的异步代码
  • 微任务:由 JS 引擎环境执行的异步代码

宏任务和微任务具体划分:

image-20230222184920343.png

宏任务每次在执行同步代码时,产生微任务队列,清空微任务队列任务后,微任务队列空间释放!

下一次宏任务执行时,遇到微任务代码,才会再次申请微任务队列空间放入回调函数消息排队

总结:一个宏任务包含微任务队列,他们之间是包含关系,不是并列关系

image-20230222184949605.png

事件循环-经典面试题

关于事件循环的经典面试题,根据代码回答打印顺序

image.png

  • 答案是1、7、5、6、2、3、4

执行顺序

  1. 第一行的console.log(1)为同步操作,进入调用栈,直接执行,打印1
  2. 第二部分setTimeout()为异步操作,进入浏览器宿主环境,因为是延时0ms,所以直接进入宏任务队列,等待执行
  3. 第三部分新的Promise()对象,因为Promise本身是同步操作,所以会执行,内部的setTimeout()为异步,与上面的一致,所以进入宏任务队列,resolve(5)执行
  4. 第四部分p.then()为异步操作,进入微任务队列,等待执行
  5. 第五部分p2创建新的Promise()对象,resolve(6)执行,p2.then()是异步操作,进入微任务队列
  6. 最后的console.log(7)为同步任务,直接执行,打印7
  7. 当前调用栈的所以任务已经执行完毕,接下来先执行微任务队列,执行第四部分的p.then(),因为resolve传入的是5,所以console.log(result)执行打印5
  8. 接下来继续执行微任务队列,执行第五部分的p2.then(),同理打印6
  9. 微任务队列执行完毕,开始执行宏任务队列,执行第二部分的setTimeout(),这个函数内部的console.log(2)为同步任务,所以直接执行,打印2,后面的p.then()再次进入微任务队列
  10. 当前微任务队列有一个任务,所以先执行微任务,另外的宏任务继续等待,微任务第二部分setTimeout()当中的p.then()执行,打印3
  11. 最后微任务队列执行完毕,开始执行宏任务队列的最后一个任务,第三部分Promise()内部的setTimeout()打印4

Promise.all 静态方法

概念:合并多个 Promise 对象,等待所有同时成功完成(或某一个失败),做后续逻辑

image-20230222190117045.png

1
2
3
4
5
6
const p = Promise.all([Promise对象, Promise对象, ...])
p.then(result => {
// result 结果: [Promise对象成功结果, Promise对象成功结果, ...]
}).catch(error => {
// 第一个失败的 Promise 对象,抛出的异常对象
})