特别说明,这篇博客是我个人对JavaScript异步操作的总结和分类。通过这篇文章,也希望读者能够从宏观的角度,看一看JavaScript异步操作是如何演进的。但是如果你想通过这篇博客掌握所有的promise或者asyncfunction等其他技术的知识,还是不太现实。推荐大家精读阮一峰先生的《ECMAScript6入门-Promise对象》和Nicholas先生的《深入理解 ES6》第11章(第219页)。Abstract用哲学的口吻说:JavaScript异步操作进化的最终目标是让异步代码看起来更像同步代码。JS语言单线程运行的本质并没有让使用它的人觉得它鸡肋,反而让程序员可以创造出各种各样的工具来提高它的性能。从回调函数到Promise对象再到异步函数被认为是JS异步操作的终极解决方案。每一个都是从无到有,从社区到标准。本篇博客将从源头入手,先讨论为什么JS需要异步操作。然后解释一些概念术语。最后,我们来看一下JS异步操作的开发过程。关键词同步|异步|事件循环|回调|承诺|发电机|Async/AwaitJavaScript为什么需要异步操作单线程JavaScript?其中的亮点之一是DOM操作。想象一下,一段代码正在修改DOM,而另一段代码需要删除DOM。那么你应该听谁的?为了避免更复杂的线程同步问题,JS执行环境只有一个线程负责执行代码。这就是我们常说的JavaScript单线程工作模式。在工作模式下,有时在执行一些耗时的任务时,需要等待当前任务完成后再进入下一个任务。这样程序就会出现假死,也就是我们常说的阻塞。为了避免这种情况,JS将工作模式分为两类:同步模式和异步模式。一些概念同步模式下执行的同步代码会在执行栈中排队等待执行。也就是我们常说的压栈运行,运行完成后出栈。闭包函数可以在执行堆栈中保留一个变量。异步模式下的代码是不会进入主线程的,也就是我们的执行栈。相反,它进入任务队列或消息队列。当执行栈中的所有同步任务都执行完毕后,系统会读取任务队列,那些异步代码就会结束等待,进入执行栈,开始执行。注:同步或异步是指运行环境提供的API是工作在同步模式还是异步模式。同步API:console.log()异步API:setTimeOut()Stack执行栈。主线程运行时会产生堆和栈,栈中的代码调用各种外部API,将各种事件添加到任务队列中。消息队列消息队列。WebAPI浏览器提供的各种API接口。事件循环只要栈中的代码执行完毕,主线程就会从消息队列中读取异步操作。这个过程是循环的,所以整个运行机制也叫事件循环——EventLoop。栈、消息队列、事件循环、webapi的关系可以参考下图:演化历史CallBack回调函数是最早实现异步操作的。它是由调用者定义并交给执行者执行的函数。几种常见的应用有:事件机制和发布-订阅模式等。varxhr=newXMLHttpRequest();xhr.open('GET',url);xhr.onload=function(){...}//接收到完整响应数据时触发的回调函数xhr.onerror=function(){...}//接收到完整响应数据时触发的回调函数xhr.send()请求发送错误缺陷:当我们需要发送多个请求,并且当这些请求都返回成功时,我们需要检查所有的请求结果在做某些处理时,难免会想到一些特殊的技巧。最简单的方法是嵌套每个请求,当一个请求成功时执行下一个请求。这样实现的问题,首先会是浪费时间,其次,也会形成我们常说的回调地狱,使得代码既不美观,也难以维护。/*第一层*/$.ajax({url:URL1,/*callback1*/success:function(){/*第二层*/$.ajax({url:URL2,/*callback2*/success:function(){.../*leveln*/$.ajax({...})}})}})PromisePromise是一个对象,是一个异步操作结果的占位符。用于在异步任务结束后指示成功或失败。Promise的状态一旦确定,就无法修改。Promise的生命周期:当执行一个异步操作时,它承诺给出一个结果。在给出最终结果之前,称为待定状态。有两种结果,成功完成状态和失败拒绝状态。结果给出后,需要做一些反应(占任务),对应onFulfilled和onRejected。then()方法:当Promise的状态发生变化时,您可以使用then()方法执行一些特定的操作。Promise的本质是使用回调函数来定义异步任务结束后需要执行的任务。functionajax(url){returnnewPromise(function(resolve,reject){varxhr=newXMLHttpRequest()xhr.responseType='json'xhr.onload=function(){if(this.status===200){resolve(this.response)}else{reject(newError(this.statusText))}}xhr.send()})}ajax('/api/user.json').then(function(res){控制台。log(res)},function(error){console.log(error)})串联的PromisePromise对象的then()方法将返回一个全新的Promise对象。后面的then()方法就是为前面的then返回的Promise注册回调,前面的then()方法中回调函数的返回值会作为后面的then()方法回调的参数。如果回调返回一个Promise,后面的then()方法回调会等待结束,所以Promise可以链式调用。每一个then()方法其实都是在state被清除后,给上一个then()方法返回的Promise对象添加一个回调。letp1=newPromise(function(resolve,reject){resolve(42);})p1.then(function(value){console.log(value)}).then(function(){console.log("Finished")})//42//完成捕获错误。当Promise失败或发生异常时,将执行onRejected回调。catch()方法相当于then()方法接受的第二个参数,但不同的是Promise链中的catch()方法允许我们捕获前一个Promise的fulfillment或rejectionhandler中发生的错误.letp1=newPromise(function(resolve,reject){thrownewError("Explosion")})p1.catch(function(error){console.log(error.message)thrownewError("Boom")}).catch(function(error){console.log(error.message)})Promise静态方法/*Promise.resolve()*/letpromise=Promise.resolve(42)promise.then(function(value){console.log(value)//42})/*Promise.reject()*/letpromise=Promise.reject(42)promise.catch(function(value){console.log(value)//42})/*下面这两种写法是等价的*/Promise.resolve('foo').then(function(value){console.log(value)})newPromise(function(resolve,reject){resolve('foo')})PromiseExecutioninParallel/*Promise.all()*该方法以单个可迭代对象(如数组)为参数,返回一个Promise。*返回的Promise直到iterable中的所有Promise元素都被fulfilled后才会被fulfill。*/letp1=newPromise(function(resolve,reject){resolve(42)})letp2=newPromise(function(resolve,reject){reject(43)})letp=Promise.all([p1,p2])p.catch(function(value){console.log(value)//43})/*Promise.race()*这个方法也接受一个Promise可迭代对象并返回一个新的Promise。*一旦其中一个源Promise得到解决,返回的Promise就会得到解决。*/letp1=Promise.resolve(42)letp2=newPromise(function(resolve,reject){resolve(43)})letp=Promise.race([p1,p2])p.then(function(value){console.log(value)//42})宏任务和微任务回调队列中的任务称为宏任务。在执行宏任务的过程中,可以临时增加一些额外的需求。可以选择作为新的宏任务进入任务队列,也可以作为当前任务的微任务进入任务队列,当前任务结束后立即执行。微任务可以提高整体响应能力,Promise回调将作为微任务执行。可以使用setTimeOut()添加宏任务。console.log("globalstart")setTimeOut(()=>{console.log("setTimeOut")},0)Promise.resolve().then(()=>{console.log("Promise")}).then(()=>{console.log("Promise2")})console.log("globalend")//globalstart//globalend//Promise//Promise2//setTimeOutGeneratorGenerator生成器执行过程:当定义,函数名前有一个*。当调用Generator函数时,函数不会立即执行,而是会得到一个generator对象。当我们调用生成器对象的next()方法时,它会一直执行。会执行到yield关键字所在的位置,返回yield后的值,然后这个函数就会暂停执行。yield的返回值会以{value:"foo",done:false}的形式接收到,当我们再次调用next()方法并传入参数时,函数就会继续执行,而我们传入的参数in会作为yield的返回值。如果我们在外面调用generator对象的throw()方法,那么函数就会得到这个异常。您可以在函数内部使用try...catch...来捕获异常function*main(){constusers=yieldajax('/api/users.json')console.log(users)constposts=yieldajax('/api/posts.json')console.log(posts)}constg=main()constresult=g.next()result.value.then(data=>{constresult2=g.next(data)如果(result2.done)returnresult2.value.then(data=>{...})})Async/Await执行异步函数,并返回Promise对象asyncfunctiontest1(){return1}asyncfunctiontest2(){returnPromise.resolve(2)}constresult1=test1()constresult2=test2()console.log('result1',result1)console.log('result1',result1)Promise.then()成功,对应waitasyncfunctiontest3(){constp3=Promise.resolve(3)p3.then(data=>{console.log('data',data)})constdata=awaitp3console.log('data',data)}asyncfunctiontest4(){constdata4=await4console.log('data4',data4)}asyncfunctiontest5(){consttest5=awaittest1()console.log('test5',test5)}P浪漫。在catch()异常的情况下,对应try...catch有时我们希望即使前面的(异步)操作失败了,后面的(异步)操作也不会被打断。这时候我们可以使用try...catch...来捕获异常asyncfunctiontest6(){constp6=Promise.reject(6)try{constdata6=awaitp6console.log('data6',data6)}catch(e){console.log('e',e)}}致谢感谢每一位为JavaScript做出贡献的程序员。也感谢每一位努力的“潜伏者”,期待你们的爆发。阮一峰老师Nicholas老师B站《IT课程大师》
