AJAX-进阶
同步和异步
- 同步代码:逐行执行,需原地等待结果后,才继续向下执行
- 异步代码:调用后耗时,不阻塞代码继续执行(不必原地等待),在将来完成后触发回调函数传递结果
1 | <!--这里用延时器模拟需要异步的操作--> |
- 结果就是先打印1、4,两秒之后打印2,最后点击按钮打印3
常见的异步操作:
- 回调函数
Promise
async/await
- 事件监听
- 定时器
XMLHttpRequest
和Fetch API
等网络请求- Web Workers(Web Worker API提供了从主执行线程分离并在后台运行脚本的能力,即在后台运行JavaScript代码,不影响页面UI的渲染和响应能力)
Node.js
中的异步I/O操作(如读写文件、网络请求等)
回调函数地狱
概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱
例如:
1 | axios({ url: 'http://hmajax.itheima.net/api/province' }).then(result => { |
以上代码是实现获取省份名称之后获取城市名称,再获取镇区名称的功能。出现了回调函数地狱问题。
虽然这样的代码可以正常运行,但存在以下问题:
- 可读性差:嵌套过多的回调函数使得代码难以阅读和理解,代码的意图变得模糊,不易于维护。
- 难以调试:回调函数地狱使得代码执行流程变得非常复杂,导致难以进行调试。当出现错误时,追踪问题和定位 bug 变得更加困难。
- 缺乏可扩展性:如果需要对现有代码进行修改或添加新功能,由于代码结构复杂,很容易引入更多的错误。而且,由于函数之间的紧密耦合,修改会牵一发而动全身。
- 可维护性差:回调函数地狱中的代码难以维护和修改,因为任何一处的更改都可能会导致意想不到的后果。这增加了代码维护的成本和风险。
- 这个时候就需要
Promise
、async/await
等异步编程的改进方法来管理异步操作,使代码结构更清晰、可读性更强、易于维护和扩展。
Promise-链式调用
概念:依靠 then()
方法会返回一个新生成的 Promise
对象特性,继续串联下一环任务,直到结束
细节:then()
回调函数中的返回值,会影响新生成的 Promise
对象最终状态和结果
好处:通过链式调用,解决回调函数嵌套问题
做法:每个 Promise
对象中管理一个异步任务,用 then
返回新的 Promise
对象,串联起来
1 | // 1. 创建Promise对象-模拟请求省份名字 |
async和await
定义:async
函数是使用async
关键字声明的函数。async
函数是AsyncFunction
构造函数的实例,并且其中允许使用await
关键字。async
和await
关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调动Promise
概念:在async
函数内,使用await
关键字取代then
函数,等待获取Promise
对象成功状态的结果值
1 | // 别忘了引入axios.js文件 |
错误捕获
使用:try...catch
语句标记要尝试的语句块,并指定一个出现异常时抛出的响应
语法:
1 | try { |
事件循环(EventLoop)
概念:JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其他语言中的模型截然不同,比如C和Java。
原因:JavaScript是单线程,为了让耗时代码不阻塞其他代码运行,设计了事件循环模型
事件循环机制:
- 代码执行时,先执行同步任务,然后将异步任务放入任务队列中,等待执行。
- 当所有同步任务执行完毕后,JavaScript引擎会去读取任务队列中的任务。
- 将队列中的第一个任务压入执行栈中执行,执行完毕后将其出栈。
- 如此循环执行,直到任务队列中的所有任务都执行完毕。
用一个例子再次模拟一下:
1 | console.log(1) // 同步任务,直接执行 |
- 结果为1、5、3、2、4,按下按钮打印 6。
宏任务与微任务
宏任务(MacroTasks)和微任务(Micro Tasks)是指在JavaScript中异步任务队列中执行的不同类型任务。
ES6 之后引入了 Promise 对象, 让 JS 引擎也可以发起异步任务
异步任务划分为了
- 宏任务:由浏览器环境执行的异步代码
- 微任务:由 JS 引擎环境执行的异步代码
宏任务和微任务具体划分:
宏任务每次在执行同步代码时,产生微任务队列,清空微任务队列任务后,微任务队列空间释放!
下一次宏任务执行时,遇到微任务代码,才会再次申请微任务队列空间放入回调函数消息排队
总结:一个宏任务包含微任务队列,他们之间是包含关系,不是并列关系
事件循环-经典面试题
关于事件循环的经典面试题,根据代码回答打印顺序
- 答案是1、7、5、6、2、3、4
执行顺序:
- 第一行的
console.log(1)
为同步操作,进入调用栈,直接执行,打印1 - 第二部分
setTimeout()
为异步操作,进入浏览器宿主环境,因为是延时0ms,所以直接进入宏任务队列,等待执行 - 第三部分新的
Promise()
对象,因为Promise本身是同步操作,所以会执行,内部的setTimeout()
为异步,与上面的一致,所以进入宏任务队列,resolve(5)
执行 - 第四部分
p.then()
为异步操作,进入微任务队列,等待执行 - 第五部分p2创建新的
Promise()
对象,resolve(6)
执行,p2.then()
是异步操作,进入微任务队列 - 最后的
console.log(7)
为同步任务,直接执行,打印7 - 当前调用栈的所以任务已经执行完毕,接下来先执行微任务队列,执行第四部分的
p.then()
,因为resolve
传入的是5
,所以console.log(result)
执行打印5 - 接下来继续执行微任务队列,执行第五部分的
p2.then()
,同理打印6 - 微任务队列执行完毕,开始执行宏任务队列,执行第二部分的
setTimeout()
,这个函数内部的console.log(2)
为同步任务,所以直接执行,打印2,后面的p.then()
再次进入微任务队列 - 当前微任务队列有一个任务,所以先执行微任务,另外的宏任务继续等待,微任务第二部分
setTimeout()
当中的p.then()
执行,打印3 - 最后微任务队列执行完毕,开始执行宏任务队列的最后一个任务,第三部分
Promise()
内部的setTimeout()
,打印4
Promise.all 静态方法
概念:合并多个 Promise 对象,等待所有同时成功完成(或某一个失败),做后续逻辑
1 | const p = Promise.all([Promise对象, Promise对象, ...]) |