当前位置: 首页 > 后端技术 > Node.js

现代JS中的流控制:详解回调、Promises、Async-Await

时间:2023-04-04 01:07:24 Node.js

JavaScript经常声称是_asynchronous_。这意味着什么?它如何影响发展?近年来,这种方法有何变化?考虑以下代码:result1=doSomething1();结果2=doSomething2(结果1);大多数语言同步处理每一行。第一行运行并返回结果。无论第一行完成后需要多长时间,第二行都会运行。单线程处理JavaScript在单个处理线程上运行。当在浏览器选项卡中执行时,其他一切都会停止,因为在并行线程上不会发生页面DOM的更改;将一个线程重定向到另一个URL而另一个线程试图附加子节点是很危险的。这对用户来说是显而易见的。例如,JavaScript检测按钮点击、运行计算并更新DOM。完成后,浏览器可以自由处理队列中的下一项。(旁注:PHP等其他语言也使用单线程,但可以由Apache等多线程服务器管理。对同一个PHP运行时页面的两个同时请求可以启动两个运行独立实例的线程。)回调异步单线程引发了一个问题。当JavaScript调用浏览器中的Ajax请求或服务器上的数据库操作等“慢”过程时会发生什么?此操作可能需要几秒钟-甚至几分钟。浏览器在等待响应时被锁定。在服务器上,Node.js应用程序将无法进一步处理用户请求。解决方案是异步处理它。不是等待完成,而是告诉进程在结果准备好时调用另一个函数。这称为回调,它作为参数传递给任何异步函数。例如:doSomethingAsync(callback1);console.log('finished');//callwhendoSomethingAsynccompletesfunctioncallback1(error){if(!error)console.log('doSomethingAsynccomplete');}doSomethingAsync()接受一个回调函数作为参数(仅供参考传递给函数,所以几乎没有开销)。doSomethingAsync()需要多长时间并不重要;我们所知道的是callback1()将在未来的某个时间点执行。控制台将显示:finisheddoSomethingAsynccomplete回调地狱通常,回调只能由异步函数调用。因此,您可以使用简洁的匿名内联函数:doSomethingAsync(error=>{if(!error)console.log('doSomethingAsynccomplete');});通过嵌套回调函数,可以串行完成一系列的两个或多个异步调用。例如:async1((err,res)=>{if(!err)async2(res,(err,res)=>{if(!err)async3(res,(err,res)=>{console.log('async1,async2,async3完成。');});});});不幸的是,这引入了回调地狱——一个臭名昭著的概念(http://callbackhell.com/)!代码难以阅读,而且当您添加错误处理逻辑时,情况会变得更糟。回调地狱在客户端编码中相对少见。如果您正在进行Ajax调用、更新DOM并等待动画完成,它可能会深入两到三个层次,但通常仍然是可管理的。对于操作系统或服务器进程,情况有所不同。Node.jsAPI调用可以接收文件上传、更新多个数据库表、写入日志以及在发送响应之前进行进一步的API调用。PromisesES2015(ES6)引入了Promises。仍然可以使用回调,但Promises为链接异步命令提供了更清晰的语法,因此它们可以串行运行(更多信息请参见此处)。要启用基于Promise的执行,必须更改基于异步回调的函数,以便它们立即返回Promise对象。这个promises对象在未来的某个时刻运行两个函数之一(作为参数传递):resolve:当进程成功完成时运行的回调函数reject:当失败发生时运行的可选回调函数。在下面的示例中,数据库API提供了一个接受回调函数的connect()方法。外部asyncDBconnect()函数立即返回一个新的Promise并在连接建立或失败时运行resolve()或reject():constdb=require('database');//连接到数据库functionasyncDBconnect(param){returnnewPromise((resolve,reject)=>{db.connect(param,(err,connection)=>{if(err)reject(err);elseresolve(connection);});});}Node.js8.0+提供util.promisify()实用程序,可将基于回调的函数转换为基于Promise的替代方法。有几个条件:将回调作为最后一个参数传递给异步函数回调函数必须指向一个错误,后跟一个值参数。Example://Node.js:promisifyfs.readFileconstutil=require('util'),fs=require('fs'),readFileAsync=util.promisify(fs.readFile);readFileAsync('file.txt');各种客户端库也提供promisify选项,但您可以自己创建几个://promisify作为最后一个参数传递的回调函数//回调函数必须接受(err,data)参数functionpromisify(fn){returnfunction(){returnnewPromise((resolve,reject)=>fn(...Array.from(arguments),(err,data)=>err?reject(err):resolve(data)));}}//examplefunctionwait(time,callback){setTimeout(()=>{callback(null,'done');},time);}constasyncWait=promisify(wait);aysc等待(1000);asyncchain任何返回Promise的东西都可以发起一系列在.then()方法中定义的异步函数调用。每个都传递前一个解决方案的结果:asyncDBconnect('http://localhost:1234').then(asyncGetSession)//passedresultofasyncDBconnect.then(asyncGetUser)//passedresultofasyncGetSession.then(asyncLogAccess)//asyncGetUser的传递结果.then(result=>{//非异步函数console.log('complete');//(asyncLogAccess的传递结果)returnresult;//(结果传递给next.then())}).catch(err=>{//在任何拒绝时调用console.log('error',err);});同步函数也可以在.then()块中执行。返回值将传递给下一个.then()(如果有)。.catch()方法定义了一个函数,在触发任何先前的拒绝时调用该函数。此时不会再运行.then()方法。您可以在整个链中使用多个.catch()方法来捕获不同的错误。ES2018引入了一个.finally()方法,它运行任何最终逻辑而不管结果如何——例如清理,关闭数据库连接等。目前只支持Chrome和Firefox,但是TC39发布了一个.finally()polyfill.functiondoSomething(){doSomething1().then(doSomething2).then(doSomething3).catch(err=>{console.log(err);}).finally(()=>{//在这里整理一下!});}使用Promise.all()进行多个异步调用Promise.then()方法一个接一个地运行异步函数。如果顺序无关紧要——例如,初始化不相关的组件——同时启动所有异步函数并在最后一个(最慢的)函数运行结束时结束会更快。这可以通过Promise.all()来实现。它接受一个函数数组并返回另一个Promise。例如:Promise.all([async1,async2,async3]).then(values=>{//解析值数组console.log(values);//(与函数数组顺序相同)return价值观;})。catch(err=>{//在任何拒绝时调用console.log('error',err);});如果任何一个异步函数调用失败,Promise.all()将立即终止。使用Promise.race()的多个异步调用Promise.race()与Promise.all()类似,只是它会在第一个Promise解决或拒绝时立即解决或拒绝。只有最快的基于Promise的异步函数才会完成:Promise.race([async1,async2,async3]).then(value=>{//singlevalueconsole.log(value);returnvalue;}).catch(err=>{//调用任何拒绝console.log('error',err);});但是还有什么不对吗?Promises减少了回调地狱但引入了其他问题。教程通常没有提到整个Promise链是异步的。任何使用一系列promise的函数都应该返回自己的Promise或在最终的.then()、.catch()或.finally()方法中运行回调函数。学习基础知识至关重要。有关Promises的更多资源:MDNPromise文档JavaScriptPromises:简介JavaScriptPromises...相关详细信息Promises异步编程Async/AwaitPromises可能令人望而生畏,因此ES2017引入了async和await。虽然它可能只是语法糖,但它使Promise更加完整,您可以完全避免.then()链。考虑以下基于Promise的示例:asyncLogAccess).then(result=>resolve(result)).catch(err=>reject(err))});}//runconnect(自执行函数)(()=>{connect();.then(结果=>console.log(result)).catch(err=>console.log(err))})();用这个重写async/await:外部函数必须以async语句开头,对于异步的Promise-based函数的调用必须在await之前,以保证在执行下一个命令之前处理完成。asyncfunctionconnect(){try{constconnection=awaitasyncDBconnect('http://localhost:1234'),session=awaitasyncGetSession(connection),user=awaitasyncGetUser(session),log=awaitasyncLogAccess(user);返回日志;}catch(e){console.log('error',err);返回空值;}}//运行连接(自执行异步函数)(async()=>{awaitconnect();})();await有效地使每个调用看起来是同步的,而不是阻塞JavaScript的单个处理线程。此外,异步函数总是返回一个Promise,因此它们可以被其他异步函数调用。async/await代码可能不会更短,但有相当大的好处:1.语法更清晰。更少的括号和更少的错误。2.调试更容易。可以在任何await语句上设置断点。3.更好的错误处理。try/catch块可以像同步代码一样使用。4.支持很好。所有浏览器(IE和OperaMini除外)和Node7.6+都支持它。但并非一切都是完美的……永远不要滥用async/awaitasync/await仍然依赖于Promises,而Promises最终依赖于回调。您需要了解Promise的工作原理,并且没有直接等同于Promise.all()和Promise.race()的方法。并且不要忘记Promise.all(),它比使用一系列不相关的await命令更高效。同步循环中的异步等待有时,您会尝试在异步函数中调用同步循环。例如:asyncfunctionprocess(array){for(letiofarray){awaitdoSomething(i);}}它不会工作。这也不行:asyncfunctionprocess(array){array.forEach(asynci=>{awaitdoSomething(i);});}循环本身保持同步并始终在其内部异步操作之前完成。ES2018引入了异步迭代器,它类似于常规迭代器,但next()方法返回一个Promise。因此,await关键字可以与for循环一起使用以串行运行异步操作。例如:asyncfunctionprocess(array){forawait(letiofarray){doSomething(i);但是,在实现异步迭代器之前,最好将数组项映射到异步函数并使用Promise.all()运行它们。例如:consttodo=['a','b','c'],alltodo=todo.map(async(v,i)=>{console.log('iteration',i);awaitprocessSomething(v);});等待Promise.all(alltodo);这具有并行运行任务的好处,但不可能将一次迭代的结果传递给另一次迭代,而且映射大型数组的成本可能很高。try/catch有什么问题?如果省略任何await失败的try/catch,异步函数将静默退出。如果你有一长串异步await命令,你可能需要多个try/catch块。另一种方法是高阶函数,它捕获错误,因此try/catch块变得不必要(感谢@wesbos的建议):asyncfunctionconnect(){constconnection=awaitasyncDBconnect('http://localhost:1234'),session=awaitasyncGetSession(connection),user=awaitasyncGetUser(session),log=awaitasyncLogAccess(user);returntrue;}//捕捉错误的高阶函数呃);});}}(async()=>{awaitcatchErrors(connect)();})();但是,在应用程序必须以不同于其他错误的方式对某些错误做出反应的情况下,此选项可能不实用。尽管有一些陷阱,async/await是对JavaScript的优雅补充。更多资源:MDNasync和awaitAsync函数——让承诺更友好TC39异步函数规范使用异步函数简化异步编码JavaScript之旅异步编程是JavaScript中不可避免的挑战。回调在大多数应用程序中都是必不可少的,但很容易陷入深度嵌套的函数中。承诺抽象回调,但有许多语法缺陷。转换现有函数可能是一件苦差事,.then()链看起来仍然很乱。幸运的是,async/await提供了清晰度。代码看起来是同步的,但它不能独占单个处理线程。它将改变你编写JavaScript的方式!(译者注:CraigBuckler讲解JavaScript的文章还不错,基本上都是用一些流行的语言和代码示例来讲解JavaScript的一些特性和一些可能出现的语法问题,有兴趣的朋友可以看看(https://www.sitepoint.com/aut...))