详解awai的异步编程场景
之前的文章说到,async和await可以取代生成器函数和yield的组合,实现优雅的异步操作写成同步写法。那异步错误的捕获又该如何处理?这篇文章我将先讲async和await的特点,然后讲解异步编程中错误的捕获。
一,async关键字
async关键字标记的函数,会变成异步函数,它的返回值和一般函数不同。
1,返回一个promise
2,返回一个thenable对象则和then方法中的resolve,或者reject有关
1.1,async的返回必然是一个promise
如果返回的是普通值,它会用promise.resolve()把它转化为promise的。
async function test(){
return 'test'
}
const a=test()
console.log(a)//Promise { 'test' }
如果返回值是promise,那就走正常的promise逻辑,看它异步操作后的结果是成功还是失败。
async function test(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject("失败了")
},1000)
})
}
const a=test()
a.then((res)=>{
console.log("成功了执行",res)
},(err)=>{
console.log("失败了执行",err)//失败了执行 失败了
})
1.2,返回一个thenable对象
如果返回值是一个thenable对象,因为会递归执行其中的then方法,最后返回的promise就是这个thenable对象then方法中处理后的promise。
async function test1(){
const thenableA = {
name: '名字哦',
then: function (resolve, reject) {
console.log(`I'am ${this.name}`);
resolve(this.name)
}
}
return thenableA
}
const a=test1()//I'am 名字哦
setTimeout(()=>{
console.log(a)//Promise { '名字哦' },注意到这里的promise取到了最终的值,而不是这个thenableA对象
})
二,await关键字
1,异步函数中可以使用await关键字,普通函数不行
2,通常await关键字后面都是跟一个Promise,这点和async的返回类似。并且await执行后返回的是该promise处理完成的value值.
await只能在async异步函数中使用,并且它返回的是promsie的结果值。这里可以看之前读取文件内容的代码:
const fs = require('fs')
function readFile(fileName){
return new Promise((resolve,reject)=>{
fs.readFile(fileName, (err,data) => {
resolve(data.toString())
})
})
}
async function test() {
//先按照顺序读取三个文件
const txt1= await readFile('./text1.txt');
const txt2= await readFile('./text2.txt');
const txt3= await readFile('./text3.txt');
//这里再用结果处理其他代码
console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件
}
test()
三,async/await的同步写法只是在同个async调用栈内生效
如下代码,成功了
在22222
之后打印。是因为await把代码暂停。只会在async这个test的调用栈中生效。
function test1(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("成功了")
},1000)
})
}
async function test(){
console.log("3333")
const a =await test1()
console.log(a)
}
console.log("11111")
test()
console.log("22222")
//打印结果
//11111
//3333
//22222
//成功了
四,asycn和await让异步操作排队完成与并行完成
4.1,串行完成异步操作
我们常规使用async/await的时候,如下代码,异步任务会按照顺序执行,一个结束之后才会执行下一个,这样会造成该调用栈内的代码非常耗时。
如下代码:
function test1(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(1)
resolve("成功了第一个")
},1000)
})
}
function test2(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(2)
resolve("成功了第二个")
},2000)
})
}
function test3(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(3)
resolve("成功了")
},3000)
})
}
async function test(){
await test1()
await test2()
await test3()
}
test()
这里的test函数中,三个异步操作会按照顺序完成,一个完成后才进行下一个,这就非常耗时,这里将花费6s的时间。在有些时候,我们更希望并行完成异步请求,把这个时间节省下来。
4.2,让异步请求并行完成
4.2.1,利用js的事件循环机制
当几个任务相互独立,没有啥依赖关系时:
function test1(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(1)
resolve("成功了第一个")
},1000)
})
}
function test2(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(2)
resolve("成功了第二个")
},2000)
})
}
function test3(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(3)
resolve("成功了第三个")
},3000)
})
}
async function test(){
const a=test1()
const b=test2()
const c= test3()
const a1= await a
const b1= await b
const c1= await c
}
test()
当我们执行const a=test1();;const b=test2();const c= test3()
的时候,因为promise执行构造函数的时候,是同步的,所以这三个异步操作已经在event Table中注册执行,可以理解为三个水壶同时插上电开始烧水了。(这个比喻可以看我这篇文章理解:js事件循环机制-宏任务微任务_笑道三千的博客-CSDN博客)
这时候,abc都是状态为pedding的promise。代码继续执行到await,因为await其实效果就是yield一样,会暂停代码的执行,内部使用的是如下:
Promise.resolve(p.value).then(res=>{
_next(res);
})
这种方式处理,也就是await是调用这个then方法,返回这个promise的结果值(等状态变成fulfilled)。
那么说,三壶水都烧完是只过了3s的时间,第一个await在第一秒后有返回,第二个是接着执行代码,然后到第二个await,接着是第三个。由此实现并行执行异步操作。
4.2.2,利用promise.all方法
另外可以使用promise.all方法来实现,其实原理是一样的。但是它能等这几个并行的异步都完成后再拿结果来统一处理。
function test1(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(1)
resolve("成功了第一个")
},1000)
})
}
function test2(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(2)
resolve("成功了第二个")
},2000)
})
}
function test3(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(3)
resolve("成功了第三个")
},3000)
})
}
async function test(){
const a=await Promise.all([test1(),test2(),test3()])
console.log(a)
}
test()
其实这也是利用的js的事件循环机制,我们来看下promise.all的实现原理:
MyPromise.all = function(promiseList) {
var resPromise = new MyPromise(function(resolve, reject) {
var count = 0;
var result = [];
var length = promiseList.length;
if(length === 0) {
return resolve(result);
}
promiseList.forEach(function(promise, index) {
MyPromise.resolve(promise).then(function(value){
count++;
result[index] = value;
if(count === length) {
//全部执行完毕后才resolve结果数组出去
resolve(result);
}
}, function(reason){
reject(reason);
});
});
});
return resPromise;
}
可以看到其内部也是使用的:
MyPromise.resolve(promise).then(function(value){
count++;
result[index] = value;
if(count === length) {
//全部执行完毕后才resolve结果数组出去
resolve(result);
}
}, function(reason){
reject(reason);
});
只是遍历数组,把所有的异步操作都执行一遍。
这两种方法其实都有一些问题。看promise.all的实现原理就能明白。当并行的任务中有一个失败后,是直接reject的,也就是返回的promise是reject的状态。结果只也是错误的信息。
对于错误的捕获,下节再讲。
4.2.3,promise.all实现并发请求并控制数量
这个来自于常见的面试题:
实现一个并发请求函数concurrencyRequest(urls, maxNum,callback),要求如下:
• 要求最大并发数 maxNum
• 每当有一个请求返回,就留下一个空位,可以增加新的请求
• 所有请求完成后,结果按照 urls 里面的顺序依次打出,可以使用回调函数处理最后的结果
const sendRequest=(tasks,max,callBack)=>{
let index=0;
let limitPool=new Array(max).fill(null);
const result=[];
//这个map控制着并行的数量,因为map这里是同步的代码,所以异步任务进入eventLoop中执行,每次正好是这个并行数量
const limitResult=limitPool.map(()=>{
return new Promise((resolve)=>{
const run=()=>{
//因为该条并行线始终没有resolve,所以这个promsie的状态没有改变
if(index>=tasks.length){
resolve()
return
}
let cur=index
let task=tasks[index++]
//递归,从而在当前异步完成/失败后继续执行剩下的
task().then((res)=>{
result[cur]=res
run()
}).catch((err)=>{
result[cur]=err
run()
})
}
run()
})
})
//promsie.all只会在limitResult,这几条并行的线都完成之后,才可以执行then方法,而then方法这时候就可以取最后的结果啦。
Promise.all(limitResult).then(()=>callBack(result))
}
//开始使用
function asyncCreate(num){
let arr=[]
const asyncFn=function(){
return new Promise((resolve)=>{
setTimeout(()=>{
console.log("代码中异步执行")
resolve("异步完成")
},1000)
})
}
for(let i=0;i<num;i++){
arr.push(asyncFn)
}
return arr
}
const funtionList=asyncCreate(10)
sendRequest(funtionList,4,res=>{
console.log(res)
})
本质上就是使用的事件循环机制注册异步事件,换汤不换药,只不过多了个并行池的概念,利用limitPool.map来控制这个并行池中并行线的数量,而promsie.all等待的则是这个并行池的几个并行线都执行结束。
另一种更简洁的方法是利用await停住代码,等并行池中有请求完成了再继续往并行池中添加请求,当最后还是利用promise.all()来保证所有请求全部完成:
async function sendRequest(tasks,limit,callback){
const promises=[]
const pool= new Set()
for(const task of tasks){
const promise=task()
promises.push(promise)//构建结果集
pool.add(promise)//构建并行池
const clean=()=>pool.delete(promise)
promise.then(clean,clean)//每个异步请求完成后,自动从并发池中清除
if(pool.size>=limit){//并发池的并行数量等于限制数量时,使用await停住代码,直到并发池有一个请求完成
await Promise.race(pool)
}
}
Promise.all(promises).then((res)=>{callback(res)})//所有的请求完成后执行回调函数取结果
}
//开始使用
function asyncCreate(num){
let arr=[]
const asyncFn=function(){
return new Promise((resolve)=>{
setTimeout(()=>{
console.log("代码中异步执行")
resolve("异步完成")
},1000)
})
}
for(let i=0;i<num;i++){
arr.push(asyncFn)
}
return arr
}
const funtionList=asyncCreate(10)
sendRequest(funtionList,4,res=>{
console.log(res)
})
五,try……catch的错误捕获
5.1,promsie的同步异常捕获
上文一直没说到异常的捕获,当我们使用promsie的时候,,如果一个 Promise 操作发生了异常,那么它将会被拒绝,此时它的状态会变成 rejected。你可以使用then方法或者catch处理错误。
function asyncFunc() {
return new Promise((resolve, reject) => {
reject('Error from asyncFunc');
});
}
const fn=res=>console.log("---",res)
asyncFunc().then(null,fn); //--- Error from asyncFunc
或者采用catch:
function asyncFunc() {
return new Promise((resolve, reject) => {
reject('Error from asyncFunc');
});
}
const fn=res=>console.log("---",res)
asyncFunc().catch(fn) //--- Error from asyncFunc
这是因为catch实际上是处理状态变成rejected的promise,其调用的是then的onRejected方法。所以catch也是微任务。
MyPromise.prototype.catch = function(onRejected) {
this.then(null, onRejected);
}
另外,在promise中直接throw new Error也是能被catch捕获到的:
function asyncFunc() {
return new Promise((resolve, reject) => {
throw new Error('Error from asyncFunc');
});
}
asyncFunc().catch((err) => {
console.error('Caught error:', err.message);//Caught error: Error from asyncFunc
});
但是按照刚刚的说法,catch不是要等promise的状态变成reject之后,才能使用catch捕获到错误吗?那这里为啥又能捕获到?
这是因为promise内部执行函数(executor)执行的时候,是这样封装的(具体可以看我这篇文章:https://juejin.cn/post/7218178695679410231#heading-21):
function MyPromise(fn) {
...//其他代码
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
当在promsie中直接throw new Error的时候,会被catch捕获到,从而执行reject方法,将promise的状态修改为rejected,继而能被catch所捕获。
其实再深一层,于 JavaScript 异常处理机制:当 throw
语句抛出异常时,JavaScript 引擎会在当前作用域中查找能够处理这个异常的代码,如果找到了 try
块内的 catch
块,那么就会进入 catch
代码块去处理这个异常;否则会将异常抛给上一层作用域,直到全局作用域。
5.2,promsie的异步异常捕获
上文的throw new Error如果修改为如下代码,将其放到一个异步操作中执行:
function asyncFunc() {
return new Promise((resolve, reject) => {
setTimeout(()=>{
throw new Error('Error from asyncFunc');
},1000)
});
}
const fn=err=>console.error('Caught error:', err.message);
asyncFunc().catch(fn);
会发现又无法捕获到这个错误,也就是没有执行console.error('Caught error:', err.message);
这行代码。
这又是为什么呢?
这得从事件循环机制和promise的原理(具体可以看我这篇文章:https://juejin.cn/post/7218178695679410231#heading-21)说起。
1,首先promise执行函数(executor)执行,遇到setTimeout是宏任务,进入宏任务队列。
2,执行asyncFunc().catch方法,上文说过,它内部调用的是promise的then方法,是个微任务,进入微任务队列
3,这个微任务先执行完成,这个catch实际上做的事情就是将它的回调函数fn封装下再push到promise的回调函数收集器里面等待执行。
4,这时候,宏任务setTimeout执行完毕,开始执行它的回调:throw new Error,它是无法被promise的执行函数(executor)的try……catch捕获的。(这个下文会讲到)。于是这个promsie的状态始终是pedding,也就自然不会执行收集器里面的回调函数,不会执行console.error('Caught error:', err.message)。
那promise的异常捕获应该如何处理呢?
实际上文已经说了。就是异步的错误使用reject来 包裹,这样处理的原理是使用闭包,利用的是promise执行reject会变更状态,同时取出收集器中的回调函数依次执行。如下代码:
function asyncFunc() {
return new Promise((resolve, reject) => {
setTimeout(()=>{
reject(new Error('Error from asyncFunc'));
},1000)
});
}
const fn=err=>console.error('Caught error:', err.message);
asyncFunc().catch(fn);//Caught error: Error from asyncFunc
5.3,try…catch不能捕获的错误类型
上文其实已经遇到了,有些错误是try…catch无法捕获到的,对于上文的异步操作我们利用了promise的catch来处理。那具体都有哪些错误是try…catch无法捕获的呢?
一句话总结就是:能捕捉到的异常必须是线程执行已经进入 try catch 但 try catch 未执行完的时候抛出来的
5.3.1 直接的语法错误不能被捕获
因为语法错误是在语法检查阶段就报错了,线程执行尚未进入 try catch 代码块,自然就无法捕获到异常。
try{
a.b
}catch(err){
console.log(err)
}
//ReferenceError: a is not defined
5.3.2 异步无法捕获
try{
setTimeout(()=>{
console.log(a.b);
}, 100)
}catch(e){
console.log('error',e);
}
console.log(111);
//output
111
Uncaught ReferenceError: a is not defined
因为,setTimeout是异步函数,而try catch其实是同步顺序执行的代码,等setTimeout里面的事件进入事件队列的时候,主线程已经离开了try catch,所以try catch是无法捕获异步函数的错误的。
5.3.3 多层try…catch,如果内层捕获到的错误未上抛,则上层无法捕获
多层 try-catch 时,会被最内层的 catch()方法捕获到,然后就不再向外层冒泡:
try {
try {
throw new Error('error');
} catch (err) {
console.error('内层的catch', err); // 内层的catch Error: error
}
} catch (err) {
console.error('最外层的catch', error);
}
5.3.4 promsie对象包裹的错误无法捕获
try catch无法捕获promise对象的错误。
function asyncFn(){
return new Promise((resolve,reject)=>{
throw new Error("错误了哈")
})
}
function test(){
try{
asyncFn()
}catch(err){
console.log("成功捕获到错误了",err)
}
}
test()// UnhandledPromiseRejectionWarning: Error: 错误了哈
这是因为promise的执行函数(executor)执行的时候,是使用try…catch包裹的,这个上文5.1最后部分也说过了,它是catch到报错之后再reject(err),并没有把错误往上层抛,结合5.3.3来看,promise实际上并没有把错误往上抛,所以外层的try…catch无法捕获到这个错误。
接下来再来看5.2中的promise中的异步错误:
function asyncFunc() {
return new Promise((resolve, reject) => {
setTimeout(()=>{
throw new Error('Error from asyncFunc');
},1000)
});
}
const fn=err=>console.error('Caught error:', err.message);
asyncFunc().catch(fn);
因为setTimeout在执行回调时,主线程早已经离开了promise内置的try…catch,所以并没有被它所捕获,promise的状态也就没有发生变更,更不会执行收集器中then的回调函数。
5.4 异步操作的错误捕获写法总结
为了能够捕获到异步操作的错误,总结起来,就是如下写:
5.4.1 仅使用pramise.catch时,手动reject错误
function asyncFunc() {
return new Promise((resolve, reject) => {
setTimeout(()=>{
reject(new Error('Error from asyncFunc'));
},1000)
});
}
const fn=err=>console.error('Caught error:', err.message);
asyncFunc().catch(fn);//Caught error: Error from asyncFunc
5.4.2 使用try…catch时,需要await
await起到的作用就是主线程停留在try…catch中,从而捕获错误。
function asyncFn(){
return new Promise((resolve,reject)=>{
throw new Error("错误了哈")
})
}
async function test(){
try{
await asyncFn()
}catch(err){
console.log("成功捕获到错误了",err)
}
}
test()//成功捕获到错误了 Error: 错误了哈
这里有个地方需要和上文联系。之前我们说过promise内部执行构造函数的时候,是如下代码:文章来源:https://www.toymoban.com/news/detail-434131.html
function MyPromise(fn) {
...//其他代码
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
这里使用了内层try…catch捕获了错误,返回的应该是reject状态的promise,并没有把错误往上层抛,理应外层的try…catch无法捕获这个错误。那现在为啥又可以捕获呢?这是因为await的效果:如果它后面是rejected状态的promise,await 表达式也会抛出错误。文章来源地址https://www.toymoban.com/news/detail-434131.html
到了这里,关于浅谈异步编程中错误的捕获的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!