当前位置:网站首页 > Android编程 > 正文

浅谈异步编程中错误的捕获

详解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/#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/#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内部执行构造函数的时候,是如下代码:

function MyPromise(fn) { 
    ...//其他代码 try { 
    fn(resolve, reject); } catch (error) { 
    reject(error); } } 

这里使用了内层try…catch捕获了错误,返回的应该是reject状态的promise,并没有把错误往上层抛,理应外层的try…catch无法捕获这个错误。那现在为啥又可以捕获呢?这是因为await的效果:如果它后面是rejected状态的promise,await 表达式也会抛出错误

到此这篇浅谈异步编程中错误的捕获的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!

版权声明


相关文章:

  • Android平台签名证书(.keystore)2024-11-29 19:36:05
  • android随机抽奖代码_随机抽奖生成器app下载|随机抽奖生成器软件下载_v1.0_9ht安卓下载...2024-11-29 19:36:05
  • Android项目中添加Dobby(inline hook)2024-11-29 19:36:05
  • Android购物车2024-11-29 19:36:05
  • 人人视频android app,人人视频安卓版2024-11-29 19:36:05
  • 《第一行代码》 第一章:第一行Android代码2024-11-29 19:36:05
  • 学编程用什么电脑合适(学编程用什么电脑最好)2024-11-29 19:36:05
  • 编程入门教学视频(编程入门教学视频百度网盘)2024-11-29 19:36:05
  • 儿童编程教学(儿童编程教学体系)2024-11-29 19:36:05
  • 学编程有用不(学编程有意义吗)2024-11-29 19:36:05
  • 全屏图片