通过第一篇文章回顾在单线程环境中编程的陷阱,以及如何绕过这些陷阱来构建健壮的JavaScriptUI。按照惯例,在本文的最后,分享5个使用async/wait写出更简洁代码的技巧。为什么单线程是一个限制?在发表的第一篇文章中,我思考了一个问题:当调用栈中有需要大量时间处理的函数调用时会发生什么?例如,假设您运行复杂的图像转换算法。虽然调用堆栈具有要执行的函数,但浏览器无法执行任何其他操作-它被阻止了。这意味着浏览器无法呈现,无法运行任何其他代码,它只是卡住了。那么你的应用UI界面就卡住了,用户体验不是很好。在某些情况下,这可能不是主要问题。一个更大的问题是,一旦您的浏览器开始处理调用堆栈中过多的任务,它可能会长时间无响应。这时候很多浏览器会报错,提示是否终止页面:其中一个正在执行,其余的将在以后执行。最常见的块单元是函数。大多数刚接触JavaScript的开发人员似乎都有这样的问题,即认为所有功能都是同步完成的,而没有考虑异步情况。以下示例:您可能知道标准Ajax请求不是同步完成的,这意味着Ajax(..)函数在代码执行时没有返回任何值来分配给变量response。等待异步函数结果的一种简单方法是回调函数:注意:实际上可以设置同步Ajax请求,但永远不要那样做。如果您设置同步Ajax请求,您的应用程序界面将被阻塞——用户将无法单击、输入数据、导航或滚动。这将阻止任何用户交互,这是一种可怕的做法。以下是同步Ajax,但请不要这样做:这里以Ajax请求为例,你可以让任何代码块异步执行。这可以通过setTimeout(callback,milliseconds)函数来完成。setTimeout函数的作用是设置一个回调函数,在毫秒之后执行,如下:(){console.log('third');}first();setTimeout(second,1000);//调用`second`after1000mstird();Output:firstthirdsecondparsingtheeventloop这里以一个有点奇怪的语句开始——尽管允许异步JavaScript代码(如上面示例中讨论的setTimeout),但在ES6之前,JavaScript本身实际上从未内置任何异步概念,并且JavaScript引擎在任何给定时刻只执行一个块。那么,谁告诉JS引擎去执行程序的代码块呢?事实上,JS引擎并不是单独运行的——它在托管环境中运行,对于大多数开发人员来说,托管环境是典型的Web浏览器或Node.js。事实上,JavaScript现在被嵌入到各种各样的设备中,从机器人到灯泡,每一种设备都代表了JS引擎的不同类型的托管环境。所有环境中都有一种内置机制,称为事件循环,它通过在一段时间内调用JS引擎来处理程序的多个块的执行。这意味着JS引擎只是一个任意JS代码的按需执行环境,它是处理事件执行和结果的宿主环境。例如,当JavaScript程序发出Ajax请求从服务器获取一些数据时,在函数中设置“响应”代码(“回调”),JS引擎告诉宿主环境:“我要推迟执行现在,但是当该网络请求完成时,将返回一些数据,请回调此函数并将数据传递给它”。然后浏览器会监听来自网络的响应,当网络请求返回内容时,浏览器会通过将回调函数插入事件循环来调度回调函数的执行。这是原理图:这些网络API是什么?本质上,它们是不可访问的线程,只能被调用。它们是浏览器的并发部分。如果您是Nojs.jsjs开发人员,这些是C++API。这样的迭代在事件循环中称为(tick)标记,每个事件只是一个函数回调。让我们“执行”这段代码,看看会发生什么:1.初始化状态全为空,浏览器控制台为空,调用栈为空2.console.log('Hi')添加到调用栈中3.执行console.log('Hi')4.c??onsole.log('Hi')从调用栈中移除。5.setTimeout(functioncb1(){...})添加到调用堆栈。6.SetTimeout(functioncb1(){...})被执行,浏览器创建一个定时器用于计时,这是WebAPI的一部分。7.setTimeout(functioncb1(){...})完成自身并从调用堆栈中删除。8.console.log('Bye')添加到调用堆栈9.执行console.log('Bye')10.console.log('Bye')从调用堆栈中删除11.至少5秒后,time处理程序完成并将cb1回调推送到回调队列。12.事件循环从回调队列中获取cb1并将其压入调用堆栈。13.执行cb1并将console.log('cb1')添加到调用堆栈。14.执行console.log('cb1')15.console.log('cb1')从调用栈中移除16.cb1从调用栈中移除快速回顾:值得注意的是,ES6规定事件循环应该如何工作,这意味着在技术上它属于JS引擎的责任,而不再仅仅扮演宿主环境的角色。这种变化的一个主要原因是在ES6中引入了Promises,它需要对事件循环队列上的调度操作进行直接、细粒度的控制。setTimeout(…)的工作原理请注意,setTimeout(…)不会自动将回调排队到事件循环中。它设置了一个计时器。当计时器到期时,环境将回调放入事件循环中,以便将来的某个滴答将接收它并执行它。请看下面的代码:setTimeout(myCallback,1000);这并不是说myCallback会在1000毫秒后立即执行,而是在1000毫秒后,将myCallback添加到队列中。但是,如果队列中有其他事件在前面添加回调,则必须等待前面和后面执行完后才能执行myCallback。异步JavaScript代码入门的文章和教程不少,推荐setTimeout(callback,0),现在你知道事件循环和setTimeout是怎么工作的了:以0毫秒作为第二个参数调用setTimeout只是延迟回调放它进入回调队列,直到调用堆栈为空。请看下面的代码:console.log('Hi');setTimeout(function(){console.log('callback');},0);console.log('再见');虽然设置了等待时间为0ms,但是浏览器控制台的结果如下:HiByecallbackES6的任务队列是什么?ES6中引入了一个叫做“任务队列”的概念。它是事件循环队列之上的一层。处理Promises的最常见的异步方式。现在只讨论这个概念,以便您在讨论使用Promises的异步行为时了解Promises是如何分派和处理的。想象一下:任务队列是附加到事件循环队列中每个令牌末尾的队列。在事件循环标记期间可能发生的某些异步操作不会导致将一个全新的事件添加到事件循环队列中,而是将一个项目(即任务)添加到当前标记的任务队列的末尾。这意味着添加另一个稍后执行的函数是安全的,它将在其他任何事情之前立即执行。任务还可以创建更多任务添加到同一队列的末尾。从理论上讲,任务“循环”(不断添加其他任务的任务等)可以全速运行,从而阻止程序获得必要的资源以移动到下一个事件循环标记。从概念上讲,这类似于在代码中表达长时间运行或***循环(如while(true)..)。任务有点像setTimeout(callback,0)“hack”,但以一种引入更明确和有保证的顺序的方式实现:稍后,但更快。回调如您所知,回调是迄今为止在JavaScript程序中表达和管理异步的最常用方式。事实上,回调是JavaScript语言中最基本的异步模式。无数的JS程序,即使是非常复杂的程序,除了有的,基本都是在回调异步的基础上写的。但是,回调方法仍然存在一些缺点,许多开发人员正在努力寻找更好的异步模型。但是,如果不了解底层是什么,就不可能有效地使用任何抽象的异步模式。在下一章中,我们将深入研究这些抽象,以说明为什么更复杂的异步模式(在后续文章中讨论)是必要的,甚至是推荐的。嵌套回调看看下面的代码:我们有一个由三个嵌套在一起的函数组成的链,每个函数代表异步系列中的一个步骤。这种代码通常被称为“回调地狱”。但“回调地狱”实际上与嵌套/缩进几乎无关,这是一个更深层次的问题。首先我们等待“点击”事件,然后我们等待计时器触发,然后我们等待Ajax响应返回,此时我们可能会再次重复所有操作。乍一看,这段代码似乎将其异步性自然地映射到以下顺序步骤:listen('click',function(e){//..});然后:setTimeout(function(){//..},500);然后:ajax('https://api.example.com/endpoint',function(text){//..});***:if(text==“hello”){doSomething();}elseif(text=="world"){doSomethingElse();}所以这种表示异步代码的顺序方式看起来更自然,不是吗?一定有这种方法吧?见Promises下面的代码:varx=1;变化=2;控制台日志(x+y);这很简单:它将x和y的值相加并将其打印到控制台。但是,如果x或y的值丢失并且仍然需要计算怎么办?例如,x和y的值需要先从服务器获取,然后才能在表达式中使用。假设我们有一个函数loadX和loadY`分别从服务器加载x和y的值。然后,一旦x和y都被加载,假设我们有一个函数sum对x和y的值求和。它可能看起来像这样(丑陋,不是吗?)。这里有一些非常重要的东西-在这个代码片段中,我们将x和y作为异步获取的值,我们正在执行一个函数sum(...)(从外部),它不关心x或y,或者它们是否立即可用。当然,这种基于回调的粗略方法还有很多不足之处。这只是一小步,我们不必判断异步请求的值是如何处理的。PromiseValue使用Promises重写了上面的例子:这段代码片段中有两层Promise。首先直接调用fetchX和fetchY,返回一个promise,传给sum。sum创建并返回一个Promise,通过调用then等待Promise,完成后,sum准备就绪(解析)并将被打印。第二层是sum(…)创建Promise(通过Promise.all([...]))并返回通过调用then(…)等待的Promise。当sum(…)操作完成后,sum传入的两个Promise执行完毕后,就可以打印出来了。这隐藏了在sum(...)中等待x和y的未来值的逻辑。注意:在sum(...)内部,Promise.all([...])调用创建了一个承诺(等待promiseX和promiseY解析)。然后在链式调用.then(...)方法中又创建了一个Promise,然后将返回的x和(values[0]+values[1])相加返回。因此,我们在sum(...)的末尾调用then(...)方法-实际上是在返回的第二个Pwwromise上运行,而不是Promise.all([...])创建的Promise。另外,虽然在第二个Promise结束时没有调用then方法,但是这里也创建了一个Promise。Promise.then(…)其实可以使用两个函数,第一个函数用于执行成功的操作,第二个函数用于处理失败的操作:如果获取x或y时出错,或者添加时如果有过程中的某种失败,sum(…)返回的Promise将被拒绝,传递给then(…)的第二个回调错误处理程序将接收来自Promise的失败信息。从外部看,由于Promise封装了时间依赖状态(等待底层值的完成或拒绝,Promise本身是时间无关的),它可以以可预测的方式组合,开发者无需关心时间或底层结果.一旦Promise被resolved,此时它就变成了一个外部不可变的值。ChainablePromises非常有用:创建一个将实现延迟2000毫秒的Promise,然后我们从第一个then(...)回调返回,这导致第二个then(...)等待2000毫秒。注意:因为Promise一旦解决后在外部是不可变的,现在可以安全地将值传递给任何一方,因为它不会被意外或恶意修改,尤其是当多方遵守承诺的正确解决方案时。一方不可能影响另一方遵守承诺决议的能力。不变性听起来像是一个学术话题,但它实际上是承诺设计中最基本和最重要的方面之一,不应该被随便忽视。是否使用Promises?关于Promises的一个重要细节是确定一个值是否是一个实际的Promise。换句话说,它是否具有与Promise相同的行为?我们知道Promise是通过newPromise(...)语法构造的,你可能认为pinstanceofPromise是一个足以判断的类型,好吧,不完全是。这主要是因为有可能从另一个浏览器窗口(如iframe)接收到一个Promise值,而那个窗口或框架有自己的Promise值,与当前窗口或框架中的Promise值不同,所以检查会无法识别Promise实例。此外,库或框架可以选择包装自己的Promises,而不是使用原生ES6Promises。事实上,Promises很可能不存在于旧浏览器的库中。Swallowerrorsorexceptions如果在Promise创建过程中发生了javascript错误(TypeError或ReferenceError),异常将被捕获,promise将被拒绝。但是如果调用then(…)方法出现JS异常错误怎么办?即使它没有丢失,您可能会发现它们的处理方式有点令人惊讶,直到您深入挖掘:看起来foo.bar()中的异常确实被吞没了,但事实并非如此。然而,还有更深层次的问题我们没有注意到。p.then(…)调用本身会返回另一个Promise,该Promise将因TypeError异常而被拒绝。处理未捕获的异常许多人会争辩说有更好的方法。一个常见的建议是Promises应该添加一个done(...),它有效地将Promise链标记为“完成”。done(…)不会创建并返回Promise,因此传递给done(..)的回调显然不会向不存在的链式Promise报告问题。Promise对象的回调链,不管是then方法还是catch方法结束,如果最后一个方法抛出错误,可能不会被捕获(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个始终位于回调链末尾的done方法,保证抛出可能出现的任何错误。ES8改进了什么?Async/await(异步/等待)JavaScriptES8引入了async/await,这使得使用Promises变得更加容易。这里简要介绍了async/await提供的可能性以及如何使用它们来编写异步代码。使用async声明一个异步函数。此函数返回一个AsyncFunction对象。AsyncFunction对象表示函数中包含的代码的异步函数。当一个用async声明的函数被调用时,它返回一个Promise。当这个函数返回一个值时,这个值只是一个普通的值,函数内部会自动创建一个promise,并使用函数返回的值来解决。当此函数抛出异常时,Promise将被抛出的值拒绝。使用async声明函数时,可以包含await符号。await暂停这个函数的执行,等待传入的Promise解析完成,然后恢复这个函数的执行,并返回解析后的值。async/wait的目的是简化使用promises的行为让我们看下面的例子:functiongetNumber1(){returnPromise.resolve('374');}//这个函数和getNumber1一样同理,throw抛出异常的函数等价于返回被拒绝的Promise的函数:在异步函数中使用,并允许同步等待Promises。如果您在异步函数之外使用Promises,您仍然需要使用then回调:您还可以使用“异步函数表达式”定义异步函数。异步函数表达式与异步函数语句非常相似,并且具有几乎相同的语法。异步函数表达式和异步函数语句之间的主要区别在于函数名,在异步函数表达式中可以省略函数名以创建匿名函数。异步函数表达式可以当生命(立即调用的函数表达式),一定义就运行。varloadData=asyncfunction(){//`rp`isarequest-promisefunction.varpromise1=rp('https://api.example.com/endpoint1');varpromise2=rp('https://api.example.com/endpoint2');//目前,两个请求都被同时触发,//现在我们必须等待它们完成varresponse1=awaitpromise1;varresponse2=awaitpromise2;returnresponse1+''+response2;}更重要的是,所有主流浏览器都支持async/await:***,重要的是不要盲目选择“***”方式来编写异步代码。了解异步JavaScript的内部结构、理解为什么异步JavaScript如此重要以及对您选择的方法的内部结构有深入的了解非常重要。与编程中的其他方法一样,每种方法都有优点和缺点。编写高度可维护、非脆弱异步代码的5个技巧1.简短代码:使用async/await可以编写更少的代码。每次使用async/await时,都会跳过一些不必要的步骤:使用.then,创建一个匿名函数来处理响应,例如//rp是一个请求Promise函数。rp('https://api.example.com/endpoint1').then(函数(数据){//...});和//`rp`isarequest-promisefunction.varresponse=awaitrp('https://api.example.com/endpoint1');2、错误处理:async/wait可以使用相同的代码结构(著名的try/catch语句)同时处理同步和异步错误。查看它如何与Promises一起工作:functionloadData(){try{//Catchessynchronouserrors.getJSON().then(function(response){varparsed=JSON.parse(response);console.log(parsed);})。catch(function(e){//Catchesasynchronouserrorsconsole.log(e);});}catch(e){console.log(e);}}with:asyncfunctionloadData(){try{vardata=JSON.parse(awaitgetJSON());console.log(data);}catch(e){console.log(e);}}3.条件:用async/wait写条件代码就简单多了:functionloadData(){returngetJSON()。然后(function(response){if(response.needsAnotherRequest){returnmakeAnotherRequest(response).then(function(anotherResponse){console.log(anotherResponse)returnanotherResponse})}else{console.log(response)returnresponse}})}with:asyncfunctionloadData(){varresponse=awaitgetJSON();if(response.needsAnotherRequest){varanotherResponse=awaitmakeAnotherRequest(response);console.log(anotherResponse)returnanotherResponse}else{console.log(response);returnresponse;}}4.栈帧:与async/await不同,从Promise链返回的错误堆栈不提供有关错误发生位置的信息。看看这些:functionloadData(){returncallAPromise().then(callback1).then(callback2).then(callback3).then(()=>{thrownewError("boom");})}loadData()。catch(function(e){console.log(err);//错误:boomatcallAPromise.then.then.then.then(index.js:8:13)});和:asyncfunctionloadData(){awaitcallAPromise1()awaitcallAPromise2()awaitcallAPromise3()awaitcallAPromise4()awaitcallAPromise5()thrownewError("boom");}loadData().catch(function(e){console.log(err);//输出//错误:boomatloadData(index.js:7:9)});5.调试:如果你使用过Promises,那么你就会知道调试它们是一场噩梦。例如,如果您在程序中设置断点,然后阻塞并使用调试快捷方式(例如“停止”),调试器将不会向下移动,因为它只是“步进”同步代码。使用async/wait,您可以像处理普通同步函数一样单步执行等待调用。编辑过程中可能存在的BUG无法实时获知。之后为了解决这些bug,花费了大量的时间在日志调试上。顺便推荐一个好用的BUG监控工具Fundebug。
