浏览器中基于回调的异步JavaScript程序是典型的事件驱动程序,即它们在真正执行之前等待用户点击触发器,这意味着它们经常需要停止计算,等待事件发生。例如,对于基于HTTP的网络事件,下面的代码就是一个典型的基于回调的异步:xhr.open(方法,网址);xhr.onload=function(e){if(this.status<300)typeofdone=="function"&&done(this.response,e);否则typeoffail=="function"&&fail(e);};xhr.onabort=xhr.onerror=xhr.ontimeout=失败;xhr.send(JSON.stringify(args));returnxhr;}上面的代码onload是我们对即将到来的响应的处理和监听,done和fail表示我们的网络事件响应成功,失败时回调。最基本的异步用法传递给回调函数。那么写异步回调有什么问题呢?1.异步识别问题。如果不知道onload是一个“监视器”,传入DOM渲染操作,结果可能会导致性能问题。2.执行顺序的问题。如果知道onload是一个“监视器”,传入的回调函数似乎总是延迟。这不是一个直观的线性代码结构。3.控制反转的问题,如果我知道onload是一个“监视器”,但不知道它会如何处理我的回调函数,执行多次?不执行?4.二次嵌套问题。如果下一次请求依赖于上一次请求的数据,异步回调会在异步回调中继续执行。如果有多层嵌套,代码会变得复杂,难以维护。5.异常处理的问题,异步函数实际上是脱离了当前函数执行栈的上下文,一旦出错,很难捕捉到错误。对于问题1,可以参考文档解决。我们重点分析其他几个问题的可能结果。当使用回调传入自己的API时,当然可以控制API代码何时调用回调,如何调用,调用多少次。但是,在使用第三方API时,会出现一个“信任”问题,也就是所谓的controlReverse,传入的回调函数是否可以再次执行?如果执行多次怎么办?为了避免这样的“信任”问题,你可以在你的回调函数中加入多重判断,但是如果因为某个错误导致回调函数没有执行呢?如果这个回调函数有时同步执行有时异步执行怎么办?对于这些情况,你可能不得不在回调函数中做一些处理,每次执行回调函数时都做一些处理,这就带来了很多重复的代码。毕竟JavaScript使用回调函数承载异步是一个不可控的“黑盒子”,我们不知道里面会发生什么,出于“信任”,我们只会毫无保留的给它我们的回调,让它“接力”下去。如果回调函数是异步的,异步回调中可能还有回调,那么就会有回调嵌套,需要仔细看回调嵌套的代码,了解它的执行顺序。在实际项目中,代码会比较乱。为了排查问题,需要绕过很多突兀的内容,不断地在函数之间跳转,使得排查困难成倍增加。当然,造成这个问题的原因其实是因为这种嵌套的写法有悖于人的线性思维方式,以至于我们不得不花更多的精力去思考真正的执行顺序,代码上的嵌套和缩进只是一种分心而已在这个思考过程中。当然,违背人类的线性思维方式还不是最糟糕的事情。其实代码中会加入各种逻辑判断。当我们在这个过程中加入这些判断的时候,很快代码就会变得复杂到无法维护和更新的地步,所以尽量避免回调的嵌套。嵌套带来的另一个问题是回调地狱。其带来的问题远不是嵌套带来的可读性降低和维护难度大。代码变得难以重用。重用链接也非常困难,牵一发而动全身。并且堆栈信息断开。当函数被执行时,函数的执行上下文将被创建并压入堆栈。函数执行完毕后,执行上下文会被弹出栈。如果函数A中调用了函数B,JavaScript会先将函数A的执行上下文压入栈中,然后再将函数B的执行上下文压入栈中。当函数B执行时,函数B的执行上下文会被弹出栈。函数A执行完成后,将函数A的执行上下文出栈。这样做的好处是,如果代码执行被中断,可以检索完整的堆栈信息,并可以从中获取所需的任何信息。但是,异步回调函数并非如此。比如异步执行时,回调函数实际上是加入到任务队列中,代码一直执行到主线程执行完毕。然后完成的任务将从任务队列中选择并添加到堆栈中。此时栈中只有这个执行上下文,它的调用者根本不在调用栈中。如果回调报错,则无法向调用者抛出异常,也无法获取调用异步操作时栈中的信息,不易判断哪里出了问题。另外,因为是异步的,所以不能直接用trycatch语句捕获错误。一种补救措施是使用回调参数和返回值来密切跟踪和传播错误,但这很麻烦且容易出错。另一种补救方法可以是使用外层变量来确定回调的层级和位置捕获,但是当多个异步计算同时进行时,因为无法预期完成的顺序,所以必须使用外层作用域的变量,但是外作用域的变量也有可能被同一作用域内的其他函数访问和修改,很容易造成误操作。新诺言的出现,总是为了解决旧办法难以调和的矛盾。比如诺言。Promise有效的解决了控制反转的问题。控制反转的本质是API内部的回调如何不可信。它是对异步状态的被动接受。称之为Promise范式,希望不受信任的API只需要给出一个异步结果,代码就会主动接管。在promise中使用then来接受回调。那么问题是反转的结果是否可信?Promise中一共有三种状态。只有异步操作的结果决定了它当前处于哪个状态。任何操作都不能改变它,状态一旦改变,就不会再改变。适用于一次性事件(所以Promise不适合多次触发)事件,但是ES18加入了for-await支持异步迭代),从而保证then中的回调只能执行一次,然后可以确保then必须是异步的,这保证了Promise反转后的结果是可信的!下面是promise打包的ajax:functionajax(url){returnnewPromise((resolve,reject)=>{varxhr=newXMLHttpRequest();xhr.open(method,url);xhr.onload=()=>resolve(xhr.responseText);xhr.onerror=()=>reject(xhr.statusText);xhr.onabort=xhr.onerror=xhr.ontimeout=fail;xhr.send(JSON.stringify(args));returnxhr;});};//使用ajax('xxxURL').then(res=>{if(res.status<300){typeofdone=="function"&&done(this.response,e);}else{typeoffail=="function"&&fail(e);}}).catch((e)=>{thrownewError('error:',e)})//使用fetch会直接返回一个promiseobject,所以更简洁fetch(method,url).then((res)=>{if(res.status<300){typeofdone=="function&》;&&done(this.response,e);}else{typeoffail=="function"&&fail(e);}}).catch((e)=>{thrownewError('error:',e)})promise的另一个优势在于简单的容错机制。在回调地狱中的错误处理中不容易判断哪里出错了(不知道是异步错误还是回调错误),异步条件下不能使用trycatch语句。另外回调需要对每个错误进行预处理,所以传入的第一个参数必须是错误对象,因为原来的上下文已经结束,无法捕获错误,只能将Promise作为参数传递一次链式callinrejectsorthrowserror,直接在catch实例方法中处理,不用手动传参或复杂的容错判断。我们来看一个真实更细化的错误处理案例:fetch('xxxurl')//p1//1%的失败概率是正常的,属于可恢复错误,不应该直接抛出c1,wait=p4,p5。catch(e=>wait(500).then(fetch('xxxurl'))//响应处理p2,b1.then(res=>{if(res.status>=300){returnnull}lettype=res.headers.get('content-type');if(type!=='application/json'){thrownewTypeError('balalala')}returnres.json()//返回一个promise})//解析响应的p3,b2.then(profile=>{if(profile){done(profile)}else{fail(profile)}})//不可恢复的错误总结c2,b3.catch(e=>{if(einstanceofNetworkError){//网络故障}elseif(einstanceofTypeErroe){//格式问题}else{//其他意外错误}})上面代码的解释:第一种情况网络故障无法恢复,p1NetworkError=>c1=>p4,p5reject=>p2,p3reject=>c2=>b3第二种情况可以恢复小概率网络负载问题,p1NetworkError=>c1=>p4,p5resolve=>p3=>b1=>p3=>b2。第三种情况404,p2resolve=>b1null=>p3resolve=>b2。第四种情况响应类型问题,p2TypeError=>p3reject=>c2=>b3。Generator在没有Generator之前,Promise似乎只是一个回调包装器。其实质就是把代码包装成一个回调函数。由于是异步的,缺点是和函数的执行上下文栈是分开的,把前面那个只能通过传参的结果传给后面的result。Promise并没有解决这个本质问题,而是交给了Generator。如果函数遇到异步挂起执行异步代码,等待异步结果恢复执行,回调嵌套就会消失,代码不会被分割,异步函数会像同步函数一样写。Generator就是基于这样的考虑。与以往的异步处理方式有根本性的变化,保证了执行上下文。下面的代码是用Generator转换的ajax:functionajax(method,url){letxhr=newXMLHttpRequest();xhr.open(方法,网址);xhr.onload=function(e){if(this.status<300)returnthis.responseelsereturne};xhr.onabort=xhr.onerror=xhr.ontimeout=失败;xhr.send(JSON.stringify(args));}function*gen(method,url){letres=yieldajax(method,url)letres1=JSON.parse(res)letres2=yieldajax(res1.data)letres3=JSON.parse(res2)return[res1,res3]}varg=gen(method,url)//遇到第一个yield停止执行g.next()//手动执行第一个yieldg.next()//手动执行第二个yieldGenerator最大的问题就是再次获取执行权的问题,因为它返回的是一个遍历器对象,所以每次都需要手动获取,异步后不会自动获取执行权。可以结合callback或者Promise来获取自动执行权。Thunk函数和co模块用于实现Generator的自动化进程管理。与Promise结合会结合两者的共同优点,如统一的错误处理、控制反转和倒置、支持并发等。async&awaitasync&await内置了一个自动执行器,async返回一个Promise,这取决于在await的promise结果上,相当于Promise.resolve(awaitPromise),所以整体看起来像是Generator和promise的语法糖包。下面是一个babelpolyfill示例:asyncfunction_getData(method,url){varres1=awaitajax(method,url)varres2=awaitajax(method,url,res1.data)console.log('asyncend')}_getData(method,url).then((res)=>{//resultres1}).then((res1)=>{//resultres2}).catch((e)=>{//error})最终分析lowerupperrowkankatababel:functionasyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{varinfo=gen[key](arg);varvalue=info.value;}catch(错误){拒绝(错误);返回;}//自执行if(info.done){resolve(value);}else{Promise.resolve(value).then(_next,_throw);}}function_asyncToGenerator(fn){returnfunction(){varself=this,args=arguments;//返回的Promise包装newPromise(function(resolve,reject){vargen=fn.apply(self,args);function_next(value){asyncGeneratorStep(gen,resolve,拒绝,_next,_throw,“下一个”,值);}function_throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,“throw”,err);}_next(未定义);});};}function_getData(){//传入北京名的Generator_getData=_asyncToGenerator(function*(){yieldajax();console.log('asyncend');});返回_getData.apply(this,arguments);}
