介绍
学习async/await/promise/fetch
# 了解异步编程
JS语言的一大特点就是单线程,也就是说,同一个时间只能做一件事 这种单线程优势很多:
- 不用考虑多线程要考虑线程之间的资源抢占,死锁,冲突 线程切换等 单线程不需要考虑也无需关心线程的开销
- 不用考虑相同节点
DOM
同时触发不同的内容 (一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点 这时浏览器应该以哪个线程为准) - 相对简单 我们只需要写代码 让浏览器去做优化即可
那么缺点也很明显:
- 很多时候IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JS语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
- 于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
- 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务 也就是传统单线程
- 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
- 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。这种理念在众多单线程语言 比如
PHP,GO,Python
也有类似概念 异步任务也被称为协程- 异步任务:异步执行的任务,比如
ajax
网络请求,setTimeout
定时函数等
- 异步任务:异步执行的任务,比如
- 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务 也就是传统单线程
同步任务与异步任务的运行流程图如下:
- 从下面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环
# Promise
在es6之前 JS的异步依赖于 回调函数 但是回调函数虽然很容易理解 但是存在回调地狱(Callback Hell)
- 比如 任务1执行完 执行任务2... 那么一层一层的嵌套下去(套娃) 会让整个代码的可读性非常的差劲
# Promise
- Promise (opens new window) 是es6中新添加的异步编程
Promise
必然处于以下几种状态之一:- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled):意味着操作成功完成。
- 已拒绝(rejected):意味着操作失败。
# Promise X fetch请求操作
Fetch (opens new window) 采用了
Promise
的异步处理机制 可以代替传统的Ajax的XMLHttpRequest (opens new window)- Fetch API (opens new window) 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的一些具体部分,例如请求和响应。它还提供了一个全局
fetch()
(opens new window) 方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。
- Fetch API (opens new window) 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的一些具体部分,例如请求和响应。它还提供了一个全局
通过
fetch()
请求接口 如果请求在未来成功完成 那么会以参数的方式回调到.then
中以前我们想请求接口 要么就需要写一大堆Ajax的XMLHttpRequest (opens new window) 要么就使用
axios
这种请求库 现在可以直接用fetch()
实现接口调用
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => { // 如果请求在未来完成 那么会以参数的方式进行回调
console.log(response)
})
通过
.then
可以获得代码执行成功后返回的数据(以参数/回调的形式返回) 并且支持链式操作(Chaining) 用一种链式结构将多个成功的函数按顺序串联起来- 链式操作就是让函数依次执行 并调用上一个链接函数的返回值作为参数
- 链式操作可以避免代码的层层嵌套
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => response.json()) // 把数据转换成JSON格式 利用简写体自动return
.then((json) => console.log(json)) // 那么这个json参数 就是上面处理后的JSON数据
- 通过
.catch
可以捕获代码执行的错误- 任意一个阶段出现错误 将会进入
.catch
阶段 之后的.then
将不会执行 - 和同步编程中的try...catch (opens new window)块类似
- 任意一个阶段出现错误 将会进入
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((Response) => console.log(Response))
// 通过catch捕获错误
.catch((err) => console.log(err))
- 通过
.finally
可以在promise
链结束后调用 无论失败与否
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(resolve => resolve('success'))
.catch((err) => console.log(err))
// 通过finally进行一些promise调用后的操作
.finally(() => console.log('finally'))
# async/await
- async (opens new window) await (opens new window)是一个语法糖,通过Generator (opens new window)可以暂停的函数配合
promise
实现的,async
和await
关键字让我们可以用一种更简洁的方式写出基于Promise
(opens new window) 的异步行为,而无需刻意地链式调用promise
。 - 只要函数存在
async
那么会自动包装成promise
对象
async/await实现例子
其实就类似于生活中的排队
,咱们生活中排队买东西,肯定是要上一个人买完,才轮到下一个人。而上面也一样,在async
函数中,await
规定了异步操作只能一个一个排队执行,从而达到用同步方式,执行异步操作的效果
需求一:
async function fn () {
await request(1)
await request(2)
// 2秒后执行完
}
fn()
需求二:
async function fn () {
const res1 = await request(5)
const res2 = await request(res1)
console.log(res2) // 2秒后输出 20
}
fn()
# 创建async异步函数
async
可以创建一个异步函数 并且自动包装成promise
对象 无论函数是否有返回值
// 声明一个async异步函数
async function main () {
}
const ret = main()
// 返回值是一个Promise对象 无论函数是否有返回值
console.log(ret) // Promise {<fulfilled>: undefined}
- 使用
await
接收async
完成后的最终结果await
让我们用同步编程的方式 处理异步变成 因为await
底层也是基于promise
和事件循环机制实现的- 虽然看上去
await
会暂停函数 但是他依旧是异步编程 不影响其他函数的执行 - 如果方法外想接收
await
数据 需要把数据return
出去
// 声明一个async异步函数
async function main () {
const ret = await fetch('https://jsonplaceholder.typicode.com/posts/1')
// 这里我会等待await数据完成后再执行下面的代码
if (ret.status === 200) {
console.log('接口调用成功');
}
// async需要把数据return出去
return ret
}
main()
console.log('我不受影响 我是同步的代码')
- 那么如果你在外部直接使用
await
那么将会失去异步编程 会变成同步编程效果 会等待异步函数执行完毕后 才会进行下面的代码- 这里是
es2022
所提供的新特性 无需async
包裹 接在全局和普通函数中使用await
- 这里是
// 声明一个async异步函数
async function main () {
const ret = await fetch('https://jsonplaceholder.typicode.com/posts/1')
// 这里我会等待await数据完成后再执行下面的代码
if (ret.status === 200) {
console.log('接口调用成功');
}
// async需要把数据return出去
return ret
}
await main()
console.log('我受到影响 我要等待await数据完成后再执行下面的代码');
# 同时请求
- 同时进行两个await异步操作 这样写会打破
fetch()
的并行 因为我们会等到第一个任务执行完成后才会执行第二个任务 直接这样写会导致性能下降- 因为JS是单线程事件驱动
// 声明一个async异步函数
async function main () {
// 同时进行两个await异步操作 这样写会打破fetch()的并行
// 因为我们会等到第一个任务执行完成后才会执行第二个任务 直接这样写会导致性能下降
const ret = await fetch('https://jsonplaceholder.typicode.com/posts/1')
const ret2 = await fetch('https://jsonplaceholder.typicode.com/posts/2')
}
main ()
- 可以用Promise.all() (opens new window) 方法 同时请求两个接口 这样就不会出现顺序问题了
- 这种写法的效率会相比上面大幅提升
- 注意: 如果在同一个
async
方法中, 在调用Promise.all
之前不要在任何请求中添加await
, 如果在此之前添加await
还是会进行等待
// 声明一个async异步函数
async function main () {
// 想同时请求多个资源,可以使用Promise.all, 不需要添加await
const ret = fetch('https://jsonplaceholder.typicode.com/posts/1')
const ret2 = fetch('https://jsonplaceholder.typicode.com/posts/2')
// 写法一 统一写法
Promise.all([ret, ret2]).then(([res1, res2]) => {
// ...俩个请求都成功后的操作 可以一起写一些完成后的操作
console.log(res1, res2)
})
// 写法二 await解构拆分写法
// 使用解构赋值, 解构数组
const [res1, res2] = await Promise.all([ret, ret2])
// ...俩个请求都成功后的操作 可以分别写一些完成后的操作
console.log(res1, res2);
}
main ()
- 还可以通过Promise.race() (opens new window) 同一时间段 触发多和异步操作 但只取最先返回 和
Promise.all()
使用一样- 共同点(都是并发触发多个异步操作)
- 不同点:
Promise.all()
保证所有任务都完成后获取异步结果;Promise.race()
只要有一个任务返回就得到该任务的结果,其他任务的结果不做处理
# 循环中使用
- 循环中执行异步操作 是不能使用forEach (opens new window) 和 map (opens new window)这一类方法的
- 这类方法会直接执行异步操作 并不会按照异步顺序 一一完成后才继续执行 (比如 接口A完成后再循环调用接口B)
- 如果希望循环中的异步操作一一执行完成之后才继续执行 需要用for...of (opens new window) 循环
async function main () {
const list = [1, 2, 3]
for (let iterator of list) {
await fetch('https://jsonplaceholder.typicode.com/posts/2')
}
console.log('都完成了')
}
main()
- 多个不同的
await
还可以使用for await
实现异步操作一一执行完成之后才继续执行
// 声明一个async异步函数
async function main () {
const ret = await fetch('https://jsonplaceholder.typicode.com/posts/1')
const ret2 = await fetch('https://jsonplaceholder.typicode.com/posts/2')
const ret3 = await fetch('https://jsonplaceholder.typicode.com/posts/3')
const promise = [ret, ret2, ret3]
for await (const iterator of promise) {
// ...
}
console.log('都完成了')
}
main()
# 异常捕获
- 可以通过try...catch (opens new window) 捕获
async/await
异常
async function main () {
try {
await fetch('https://jsonplaceholder.typicode.com/posts/1')
//触发一个错误
console.log(error);
}
catch (err) {
// 捕获错误
console.log(err)
}
}
main()
# 简写
- 通过IIFE(立即调用函数表达式) (opens new window) 可以进行函数的简写并立刻调用
- 估计也没有人会用
IIFE
这种方式写函数 - 注意: 这种写法结尾必须有
;
分号 否则会被当成函数的参数 不被执行或报错
- 估计也没有人会用
// 普通写法
async function main () {
await fetch('https://jsonplaceholder.typicode.com/posts/3')
}
// 调用函数
main(); // 注意这里必须有分号 如果没有分号 IIFE(立即调用函数表达式)会被当成函数的参数 不被执行或报错
// 简写直接调用
(async () => {
await fetch('https://jsonplaceholder.typicode.com/posts/3')
})();
# 参考文献
JavaScript 运行机制详解:再谈Event Loop (opens new window)
web前端面试 - 面试官系列 (opens new window)
异步编程: 一次性搞懂 Promise, async, await (#js #javascript) (opens new window)
强烈推荐: 7张图,20分钟就能搞定的async/await原理!为什么要拖那么久? (opens new window)