浅析 Promise、Async/Await
Promise
基本用法
Promise 的简单封装与使用
1 | // 封装 |
Ma Mi 任务模型
- Ma 指 MacroTask(宏任务),Mi 指 MicroTask(微任务)
- 先 Ma 再 Mi,即先执行宏任务再执行微任务
- JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务
- 其实最初 JS 只存在一个任务队列,为了让 Promise 回调更早执行,强行又插入了一个异步的任务队列,用来存放 Mi 任务
- 宏任务:setTimeout()、setInterval()、 setImmediate()、 I/O、UI渲染(常见的定时器,用户交互事件等等)
- 微任务:Promise、process.nextTick、Object.observe、MutationObserver
Promise 的其他 API
Promise.resolve(result)
: 制造一个成功(或失败)
制造成功
1 | function 摇色子() { |
制造失败
1 | function 摇色子() { |
关于 Promise.resolve
接收参数的问题,ECMAScript 6 入门里其实说得很清楚
如果参数是 Promise 实例,那么
Promise.resolve
将不做任何修改、原封不动地返回这个实例;如果参数是一个原始值,或者没有参数,Promise.resolve
都会直接返回一个resolved
状态的 Promise 对象。
Promise.reject(reason)
: 制造一个失败
1 | Promise.reject('我错了') |
Promise.all(数组)
: 等待全部成功,或者有一个失败
全部成功,将所有成功 promise 结果组成的数组返回
1 | Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]) |
只要有一个失败,就结束,返回最先被 reject 失败状态的值
1 | Promise.all([Promise.reject(1), Promise.resolve(2), Promise.resolve(3)]) |
Promse.all
在需要对多个异步进行处理时往往非常有用;
不过在某些特殊情况下,直接使用Promse.all
就显得不那么方便了
举个例子,比如现在有 3 个请求,request1, request2 和 request3,我们需要对这 3 个请求进行统一处理,并且不管请求成功还是失败,都需要拿到所有的响应结果,如果这时候使用Promise.all([request1, request2, request3])
的话,request1 请求失败了,后面的两个请求 request2, request3 就都不会执行了(这里实际上是 request1 在 rejected
之后,被 Promise.all([]).catch
给捕获了 )。
如何解决 Promise.all()
在第一个 Promise 失败就会中断的问题?
利用 .then()
后会返回一个状态为 resolved
的 Promise(即会自动包装成一个已resolved
的promise),从而避免被 Promise.all([]).catch
捕获
1 | // 3 个请求 |
可以把对每个请求的.then
操作封装一下
1 | const x = promiseList => promiseList.map(promise => promise.then(value => ({ |
打印结果如下:
Promise.allSettled(数组)
: 等待全部状态改变
1 | Promise.allSettled([Promise.reject(1), Promise.resolve(2), Promise.resolve(3)]) |
打印结果如下:
可以看出 Promise.allSettled
的作用其实和上面我们实现的 xxx
函数的作用是一致的,因此针对上文提到场景,可以直接使用 Promise.allSettled
,更加简洁。
Promise.race(数组)
: 等待第一个状态改变
1 | Promise.race([request1(), request2(), request3()]).then((result) => { |
Promise.race([request1, request2, request3])
里面哪个请求最先响应,就返回其对应的结果,不管结果本身是成功状态还是失败状态(这里最先响应的请求是 request1)。
一般情况下用不到 Promise.race 这个 api,不过在某些场景下还是有用的。例如在多台服务器部署了同样的服务端代码,要从一个商品列表的接口拿数据,这时候就可以在 race 中写上所有服务器中的查询商品列表的接口地址,哪个服务器响应快,就优先从哪个服务器拿数据。
Promise 的应用场景
多次处理一个结果
1 | 摇色子().then(v => v1).then(v1 => v2) |
第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
串行
- 这里有一个悖论:一旦 promise 出现,那么任务就已经执行了
- 所以不是 promise 串行,而是任务串行
- 解法:把任务放进队列,完成一个再做下一个(用 Reduce 实现 Promise 串行执行)
并行
Promise.all
、Promise.allSettled
、Promise.race
都可以看作是并行地在处理任务
这里可能你会产生疑问,JS 不是单线程吗,怎么做到并行执行任务?
这里指的是并行地做网络请求的任务,而网络请求实际上是由浏览器来做的,并非是 JS 做的,就像 setTimeout 是浏览器的功能而不是 JS 的,setTimeout 只是浏览器提供给 JS 的一个接口。
Promise 的错误处理
自身的错误处理
promise 自身的错误处理其实挺好用的,直接在.then
的第二个回调参数中进行错误处理即可
1 | promise.then(s1, f1) |
或者使用.catch
语法糖
1 | // 上面写法的语法糖 |
建议总是使用
catch()
方法,而不使用then()
方法的第二个参数,原因是第二种写法可以捕获前面then
方法执行中的错误,也更接近同步的写法(try/catch
)
全局错误处理
以axios为例,Axios 作弊表
错误处理之后
- 如果你没有继续抛错,那么错误就不再出现
- 如果你继续抛错,那么后续回调就要继续处理错误
前端似乎对 Promise 不满
Async/Await替代Promise的6个理由,主要是以下 6 个方面:
- 简洁
- 错误处理
- 条件语句
- 中间值
- 错误栈
- 调试(在
.then
代码块中设置断点,使用 Step Over 快捷键,调试器不会跳到下一个.then
,因为它只会跳过异步代码)
async / await
async / await 基本用法
最常见的用法
1 | const fn = async() => { |
优点:完全没有缩进,就像是在写同步代码
封装一个 async 函数
async
的封装和使用
1 | function 摇色子() { |
用try...catch
进行错误处理
1 | async function 摇色子() { |
为什么需要 async
在函数前面加一个async
,这看起来非常多余,await
所在的函数就是async
,不是吗?
理由之一:
在 ES 标准的 async/await 出来之前,有些人自己用函数实现了 await,为了兼容旧代码里普通函数的 await(xxx)(为了将旧代码里面的 await 和新的 ES 标准里的 async/await 区分开来),其实 async 本身并没有什么意义。
你可能会说,async
函数会隐式地返回一个 Promise 对象呀,但这并不能成为必须要在函数前加async
的理由,有兴趣的可以去看看知乎上关于async
的讨论。
await 错误处理
用 try/catch 来同时处理同步和异步错误是很常见的做法
1 | let response |
但其实还有更好的写法,就像下面这样
1 | const errorHandler = error => { |
需要注意的是,
errorHandler
函数中不要直接return
一个值,一定要抛出一个错误(打断程序的运行)。因为在请求调用失败的情况下,会把errorHandler
里return
的值直接赋值给 response(通俗的说法就是“Promise 会吃掉错误”),在errorHandler
中抛出一个错误能够保证在请求成功的情况下才会有 response,请求失败的情况下一定是会进入errorHandler
函数中的
下面是一个实际的例子
1 | const ajax = function() { |
可以看到,我们仅仅只用了一句代码就可以同时处理 Promise 成功和失败的情况了,绝大多数的 ajax 调用都是可以用这样的方式来处理的。
所以,对于async/await
,并不是一定需要使用try/catch
来做错误处理的。
之前我常常陷入一个误区:就是认为await
和.then
是对立的,始终觉得用了await
后就不应该再出现.then
。
但其实并非如此,说到底async/await
也只不过是.then
的语法糖而已。就像上面的例子一样,.then
和await
完全是可以结合在一起使用的,在.then
中进行错误处理,而await
左边只接受成功结果。
另外,我们还可以把 4xx/5xx 等常见错误用拦截器全局处理,errorHandler
也可以放在拦截器里。
await 的传染性
代码:
1 | console.log(1) |
分析:
await
会使得所有它左边的和下面的代码变成异步代码console.log(3)
变成异步任务了- Promise 同样有传染性(同步变异步),放到
.then
回调函数中的代码会变成异步的,不过相比于await
,.then
下面的代码并不会变成异步的 - 回调没有传染性
await 的应用场景
多次处理一个结果
1 | const r1 = await makePromise() |
串行
天生串行(多个await
并排时,从上到下依次执行,后面的会等前面执行完了再执行)
1 | await promise1 |
并行
同 Promise,await Promise.all([p1, p2, p3])
、await Promise.allSettled([p1, p2, p3])
、await Promise.race([p1, p2, p3])
都是并行的
循环的时候存在 bug
正常情况下,即便在循环中,await
也应当是串行执行的。
例如 for 循环中的 await 是串行的(后面等前面)
1 | async function runPromiseByQueue(myPromises) { |
但是在某些循环中,如 forEach 和 map 中,await 会并行执行(后面不等前面)
1 | async function runPromiseByQueue(myPromises) { |
后面 JS 又出了一个新的东西 for await…of 来弥补这个 bug
1 | async function runPromiseByQueue(myPromises) { |