这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
Promise
基本用法
Promise 的简单封装与使用
// 封装
function 摇色子() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(Math.floor(Math.random() * 6) + 1)
}, 3000)
})
}
// 使用
摇色子().then(success1, failed1).then(success2, failed2)
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): 制造一个成功(或失败)
制造成功
function 摇色子() {
return Promise.resolve(4)
}
// 等价于
function 摇色子() {
return new Promise((resolve, reject) => {
resolve(4)
})
}
摇色子().then(n => console.log(n)) // 4
制造失败
function 摇色子() {
// 此处 Promise.resolve 接收的是一个失败的 Promise 实例(状态为 reject)
return Promise.resolve(new Promise((resolve, reject) => reject()))
}
摇色子().then(n => console.log(n)) // 1 Uncaught (in promise) undefined
关于 Promise.resolve 接收参数的问题,ECMAScript 6 入门里其实说得很清楚
如果参数是 Promise 实例,那么
Promise.resolve将不做任何修改、原封不动地返回这个实例;如果参数是一个原始值,或者没有参数,Promise.resolve都会直接返回一个resolved状态的 Promise 对象。
Promise.reject(reason): 制造一个失败
Promise.reject('我错了')
// 等价于
new Promise((resolve, reject) => reject('我错了'))
Promise.reject('我错了').then(null, reason => console.log(reason)) // 我错了
Promise.all(数组): 等待全部成功,或者有一个失败
全部成功,将所有成功 promise 结果组成的数组返回
Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])
.then(values => console.log(values)) // [1, 2, 3]
只要有一个失败,就结束,返回最先被 reject 失败状态的值
Promise.all([Promise.reject(1), Promise.resolve(2), Promise.resolve(3)])
.then(values => console.log(values)) // Uncaught (in promise) 1
Promse.all 在需要对多个异步进行处理时往往非常有用;
不过在某些特殊情况下,直接使用Promse.all就显得不那么方便了
举个例子,比如现在有 3 个请求,request1, request2 和 request3,我们需要对这 3 个请求进行统一处理,并且不管请求成功还是失败,都需要拿到所有的响应结果,如果这时候使用Promise.all([request1, request2, request3])的话,request1 请求失败了,后面的两个请求 request2, request3 就都不会执行了。
如何解决 Promise.all() 在第一个 Promise 失败就会中断的问题?
利用 .then() 后会返回一个状态为 resolved 的 Promise(即会自动包装成一个已resolved的promise)
// 3 个请求
const request1 = () => new Promise((resolve, reject) => {
setTimeout(() => {
reject('第 1 个请求失败')
}, 1000)
})
const request2 = () => new Promise((resolve, reject) => {
setTimeout(() => {
reject('第 2 个请求失败')
}, 2000)
})
const request3 = () => new Promise((resolve, reject) => {
setTimeout(() => {
resolve('第 3 个请求成功')
}, 3000)
})
Promise.all([
request1().then(value => ({ status: 'ok', value }), reason => ({ status: 'not ok', reason })),
request2().then(value => ({ status: 'ok', value }), reason => ({ status: 'not ok', reason })),
request3().then(value => ({ status: 'ok', value }), reason => ({ status: 'not ok', reason }))
]).then(result => console.log(result))
可以把对每个请求的 .then 操作封装一下
const x = promiseList => promiseList.map(promise => promise.then(value => ({
status: 'ok',
value
}), reason => ({
status: 'not ok',
reason
})))
const xxx = promiseList => Promise.all(x(promiseList))
xxx([request1(), request2(), request3()])
.then(result => console.log(result))
打印结果如下:
Promise.allSettled(数组): 等待全部状态改变
Promise.allSettled([Promise.reject(1), Promise.resolve(2), Promise.resolve(3)])
.then(result => console.log(result))
打印结果如下:
可以看出 Promise.allSettled 的作用其实和上面我们实现的 xxx 函数的作用是一致的,因此针对上文提到场景,可以直接使用 Promise.allSettled,更加简洁。
Promise.race(数组): 等待第一个状态改变
Promise.race([request1(), request2(), request3()]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 第 1 个请求失败
})
Promise.race([request1, request2, request3])里面哪个请求最先响应,就返回其对应的结果,不管结果本身是成功状态还是失败状态(这里最先响应的请求是 request1)。
一般情况下用不到 Promise.race 这个 api,不过在某些场景下还是有用的。例如在多台服务器部署了同样的服务端代码,要从一个商品列表的接口拿数据,这时候就可以在 race 中写上所有服务器中的查询商品列表的接口地址,哪个服务器响应快,就优先从哪个服务器拿数据。
Promise 的应用场景
多次处理一个结果
摇色子().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的第二个回调参数中进行错误处理即可
promise.then(s1, f1)
或者使用.catch语法糖
// 上面写法的语法糖
promise.then(s1).catch(f1)
建议总是使用
catch()方法,而不使用then()方法的第二个参数,原因是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)
全局错误处理
以axios为例,Axios 作弊表
错误处理之后
- 如果你没有继续抛错,那么错误就不再出现
- 如果你继续抛错,那么后续回调就要继续处理错误
前端似乎对 Promise 不满
Async/Await替代Promise的6个理由,主要是以下 6 个方面:
- 简洁
- 错误处理
- 条件语句
- 中间值
- 错误栈
- 调试(在
.then代码块中设置断点,使用 Step Over 快捷键,调试器不会跳到下一个.then,因为它只会跳过异步代码)
async / await
async / await 基本用法
最常见的用法
const fn = async() => {
const temp = await makePromise()
return temp + 1
}
优点:完全没有缩进,就像是在写同步代码
封装一个 async 函数
async的封装和使用
function 摇色子() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(Math.floor(Math.random() * 6) + 1)
}, 3000)
})
}
async function fn() {
const result = await 摇色子()
console.log(result)
}
fn()
用try...catch进行错误处理
async function 摇色子() {
throw new Error('色子坏了')
}
async function fn() {
try {
const result = await 摇色子()
console.log(result)
} catch (error) {
console.log(error)
}
}
fn()
为什么需要 async
在函数前面加一个async,这看起来非常多余,await所在的函数就是async,不是吗?
理由之一:
在 ES 标准的 async/await 出来之前,有些人自己用函数实现了 await,为了兼容旧代码里普通函数的 await(xxx)(为了将旧代码里面的 await 和新的 ES 标准里的 async/await 区分开来),其实 async 本身并没有什么意义。
你可能会说,async函数会隐式地返回一个 Promise 对象呀,但这并不能成为必须要在函数前加async的理由,有兴趣的可以去看看知乎上关于async的讨论。
await 错误处理
用 try/catch 来同时处理同步和异步错误是很常见的做法
let response
try {
response = await axios.get('/xxx')
} catch (e) {
if (e.response) {
console.log(e.response.status)
throw e
}
}
console.log(response)
但其实还有更好的写法,就像下面这样
const errorHandler = error => {
console.log(error)
// 注意这里要抛出一个错误
throw error
}
// 只用一句代码就可以处理成功和失败
const response = await axios.get('/xxx').then(null, errorHandler)
// 或者使用 catch 语法糖
const response = await axios.get('/xxx').catch(errorHandler)
需要注意的是,
errorHandler函数中不要直接return一个值,一定要抛出一个错误(打断程序的运行)。因为在请求调用失败的情况下,会直接把errorHandler里return的值直接赋值给 response(通俗的说法就是“Promise 会吃掉错误”),在errorHandler中抛出一个错误能够保证在请求成功的情况下才会有 response,请求失败的情况下一定是会进入errorHandler函数中的
下面是一个实际的例子
const ajax = function() {
return new Promise((resolve, reject) => {
reject('这是失败后的提示')
// resolve('这是成功后的结果')
})
}
const error = (error) => {
console.log('error:', error)
return Promise.reject(error)
}
async function fn() {
const response = await ajax().then(null, error)
console.log('response:', response)
}
fn()
可以看到,我们仅仅只用了一句代码就可以同时处理 Promise 成功和失败的情况了,绝大多数的 ajax 调用都是可以用这样的方式来处理的。
所以,对于async/await,并不是一定需要使用try/catch来做错误处理的。
之前我常常陷入一个误区:就是认为await和.then是对立的,始终觉得用了await后就不应该再出现.then。
但其实并非如此,说到底async/await也只不过是.then的语法糖而已。就像上面的例子一样,.then和await完全是可以结合在一起使用的,在.then中进行错误处理,而await左边只接受成功结果。
另外,我们还可以把 4xx/5xx 等常见错误用拦截器全局处理,errorHandler也可以放在拦截器里。
await 的传染性
代码:
console.log(1)
await console.log(2)
console.log(3) // await 会使这句代码变成异步的,如果想要让他立即执行,放到 await 前面即可
分析:
await会使得所有它左边的和下面的代码变成异步代码console.log(3)变成异步任务了- Promise 同样有传染性(同步变异步),放到
.then回调函数中的代码会变成异步的,不过相比于await,.then下面的代码并不会变成异步的 - 回调没有传染性
await 的应用场景
多次处理一个结果
const r1 = await makePromise()
const r2 = handleR1(r1)
const r3 = handleR2(r2)
串行
天生串行(多个await并排时,从上到下依次执行,后面的会等前面执行完了再执行)
await promise1
await promise2
await promise3
...
并行
同 Promise,await Promise.all([p1, p2, p3])、await Promise.allSettled([p1, p2, p3])、await Promise.race([p1, p2, p3]) 都是并行的
循环的时候存在 bug
正常情况下,即便在循环中,await也应当是串行执行的。
例如 for 循环中的 await 是串行的(后面等前面)
async function runPromiseByQueue(myPromises) {
for (let i = 0; i < myPromises.length; i++) {
await myPromises[i]();
}
}
const createPromise = (time, id) => () =>
new Promise((resolve) =>
setTimeout(() => {
console.log("promise", id);
resolve();
}, time)
);
runPromiseByQueue([
createPromise(3000, 4),
createPromise(2000, 2),
createPromise(1000, 1)
]);
// 4 2 1
但是在某些循环中,如 forEach 和 map 中,await 会并行执行(后面不等前面)
async function runPromiseByQueue(myPromises) {
myPromises.forEach(async (task) => {
await task();
});
}
const createPromise = (time, id) => () =>
new Promise((resolve) =>
setTimeout(() => {
console.log("promise", id);
resolve();
}, time)
);
runPromiseByQueue([
createPromise(3000, 4),
createPromise(2000, 2),
createPromise(1000, 1)
]);
// 1 2 4
后面 JS 又出了一个新的东西 for await...of 来弥补这个 bug
async function runPromiseByQueue(myPromises) {
// 异步迭代
for await (let item of myPromises) {
console.log('promise', item);
}
}
const createPromise = (time, id) =>
new Promise((resolve) =>
setTimeout(() => {
resolve(id);
}, time)
);
runPromiseByQueue([
createPromise(3000, 4),
createPromise(2000, 2),
createPromise(1000, 1)
]);
// 4 2 1