现代JS中的流控制:对promise的回调到Async/Await
JavaScript通常被认为是异步.这是什么意思?它如何影响发展?近年来,这种方法发生了什么变化?
考虑下面的代码:
result1 = doSomething1();result2 = doSomething2(result1);
大多数语言处理每一行同步.第一行运行并返回结果。第一行结束后,第二行开始运行不管要花多长时间.
单线程处理
JavaScript运行在单个处理线程上。当在浏览器选项卡中执行时,其他一切都会停止。这是必要的,因为对页面DOM的更改不能发生在并行线程上;当一个线程重定向到不同的URL时,另一个线程试图追加子节点,这将是危险的。
这对用户来说是不明显的,因为处理是在小块中快速进行的。例如,JavaScript检测按钮点击,运行计算,并更新DOM。一旦完成,浏览器就可以自由地处理队列上的下一项。
(注:其他语言(如PHP)也使用单线程,但可能由多线程服务器(如Apache)管理。同时对同一个PHP页面的两个请求可以启动两个运行PHP运行时独立实例的线程。)
使用回调实现异步
单个线程会产生问题。当JavaScript调用一个“慢”进程(如浏览器中的Ajax请求或服务器上的数据库操作)时会发生什么?这个手术可能需要几秒钟即使是分钟.浏览器在等待响应时会被锁定。在服务器上,Node.js应用程序将无法处理进一步的用户请求。
解决方案是异步处理。而不是等待完成,当结果准备好时,进程被告知调用另一个函数。这被称为a回调,并将其作为参数传递给任何异步函数。例如:
doSomethingAsync(callback1);控制台.日志(“完成”);// doSomethingAsync完成时调用函数callback1(错误){如果(!错误)控制台.日志(“doSomethingAsync完成”);}
doSomethingAsync ()
接受回调函数作为参数(只传递对该函数的引用,因此开销很小)。多久都不重要doSomethingAsync ()
需要;我们只知道callback1 ()
将在将来的某个时候执行。控制台将显示:
完成doSomethingAsync
你可以阅读更多关于回调的内容回到基础:JavaScript中的回调是什么?
回调地狱
通常,回调只由一个异步函数调用。因此可以使用简洁的匿名内联函数:
doSomethingAsync(错误= >{如果(!错误)控制台.日志(“doSomethingAsync完成”);});
一系列的两个或多个异步调用可以通过嵌套回调函数依次完成。例如:
async1((犯错,res)= >{如果(!犯错)async2(res,(犯错,res)= >{如果(!犯错)async3(res,(犯错,res)= >{控制台.日志('async1, async2, async3 complete.');});});});
不幸的是,这引入了回调地狱-一个臭名昭著的概念,甚至有自己的网页吗!代码难以阅读,并且在添加错误处理逻辑时将变得更糟。
回调地狱在客户端编码中相对少见。如果您正在进行Ajax调用、更新DOM并等待动画完成,那么它可能会深入两到三层,但它通常仍然是可管理的。
操作系统或服务器进程的情况不同。一个Node.js API调用可以接收文件上传,更新多个数据库表,写入日志,并在发送响应之前进行进一步的API调用。
你可以阅读更多关于回调的内容从回调地狱中拯救出来.
承诺
ES2015 (ES6)引入了Promises.回调仍然在表面之下使用,但是Promises提供了更清晰的语法链异步命令,因此它们以串联的方式运行(有关的详细信息请参见下一节).
要启用基于Promise的执行,必须修改基于异步回调的函数,使它们立即返回一个Promise对象。该对象承诺在将来的某个时候运行两个函数中的一个(作为参数传递):
解决
:处理成功完成时运行回调函数,和拒绝
:可选的回调函数,当故障发生时运行。
在下面的示例中,数据库API提供了一个connect ()
方法,该方法接受回调函数。外asyncDBconnect ()
函数立即返回一个新的Promise并运行解决()
或拒绝()
一旦连接建立或失败:
常量db=需要(“数据库”);//连接数据库函数asyncDBconnect(参数){返回新承诺((解决,拒绝)= >{db.连接(参数,(犯错,连接)= >{如果(犯错)拒绝(犯错);其他的解决(连接);});});}
Node.js 8.0+提供了一个util.promisify()实用程序将基于回调的函数转换为基于promise的替代函数。有几个条件:
- 回调必须作为最后一个参数传递给异步函数,并且
- 回调函数必须有一个错误,后跟一个值形参。
例子:
// Node.js: promisify fs.readFile常量跑龙套=需要(“跑龙套”),fs=需要(“fs”),readFileAsync=跑龙套.promisify(fs.readFile);readFileAsync(“file.txt”);
各种客户端库也提供了promisify选项,但你可以自己用几行代码创建一个:
//指定一个回调函数作为最后一个参数传递//回调函数必须接受(err, data)参数函数promisify(fn){返回函数(){返回新承诺((解决,拒绝)= >fn(...数组.从(参数),(犯错,数据)= >犯错?拒绝(犯错):解决(数据)));}}/ /实例函数等待(时间,回调){setTimeout(()= >{回调(零,“完成”);},时间);}常量asyncWait=promisify(等待);ayscWait(1000);
异步的链接
中定义的一系列异步函数调用那就是()
方法。每一个都传递前一个的结果解决
:
asyncDBconnect(“http://localhost: 1234”).然后(asyncGetSession)//传入asyncDBconnect的结果.然后(asyncGetUser)// asyncGetSession传递的结果.然后(asyncLogAccess)// asyncGetUser传递的结果.然后(结果= >{//非异步函数控制台.日志(“完成”);//(传入asyncLogAccess的结果)返回结果;//(结果传递给next .then())}).抓(犯错= >{//调用任何拒绝控制台.日志(“错误”,犯错);});
同步函数也可以在那就是()
块。返回值被传递给下一个那就是()
(如果有的话)。
的.catch ()
方法定义了一个函数,该函数在任何先前的拒绝
是解雇。在这一点上,没有进一步那就是()
方法将运行。你可以有多个.catch ()
方法来捕获不同的错误。
ES2018引入了最后()
方法,该方法运行任何最终逻辑,而不管结果如何——例如,清理、关闭数据库连接等。目前它只在Chrome和Firefox中支持,但是技术委员会39已经发布了一个最后()polyfill.
函数doSomething(){doSomething1().然后(doSomething2).然后(doSomething3).抓(犯错= >{控制台.日志(犯错);}).最后(()= >{//这里收拾一下!});}
使用Promise.all()进行多个异步调用
承诺那就是()
方法一个接一个地运行异步函数。如果顺序无关紧要(例如,初始化不相关的组件),那么同时启动所有异步函数并在最后一个(最慢的)函数运行时完成会更快解决
.
这可以通过Promise.all ()
.它接受一个函数数组并返回另一个Promise。例如:
承诺.所有([async1,async2,async3]).然后(值= >{//已解析值的数组控制台.日志(值);//(与函数数组顺序相同)返回值;}).抓(犯错= >{//调用任何拒绝控制台.日志(“错误”,犯错);});
Promise.all ()
如果任意一个异步函数调用,则立即终止拒绝
.
使用Promise.race()进行多个异步调用
Promise.race ()
类似于Promise.all ()
,除非它会解决或拒绝尽快第一个承诺解决或拒绝。只有最快的基于promise的异步函数才能完成:
承诺.比赛([async1,async2,async3]).然后(价值= >{//单个值控制台.日志(价值);返回价值;}).抓(犯错= >{//调用任何拒绝控制台.日志(“错误”,犯错);});
前途光明?
承诺减少回调地狱,却引入自己的问题。
教程通常不会提到这一点整个承诺链是异步的.任何使用一系列Promise的函数都应该返回自己的Promise,或者在final中运行回调函数那就是()
,.catch ()
或最后()
方法。
我也要坦白一件事:承诺让我困惑了很长时间.语法通常看起来比回调更复杂,有很多错误,调试可能会有问题。然而,学习基础知识是必要的。
进一步承诺资源:
异步/等待
承诺可能令人生畏,所以ES2017介绍了异步
而且等待
.虽然它可能只是语法上的糖,但它让承诺变得更甜,你可以避免那就是()
链。考虑下面这个基于promise的例子:
函数连接(){返回新承诺((解决,拒绝)= >{asyncDBconnect(“http://localhost: 1234”).然后(asyncGetSession).然后(asyncGetUser).然后(asyncLogAccess).然后(结果= >解决(结果)).抓(犯错= >拒绝(犯错))});}// run connect(自动执行函数)(()= >{连接();.然后(结果= >控制台.日志(结果)).抓(犯错= >控制台.日志(犯错))})();
用异步
/等待
:
- 外部函数之前必须有
异步
声明, - 对基于promise的异步函数的调用必须在
等待
以确保在执行下一个命令之前完成处理。
异步函数连接(){试一试{常量连接=等待asyncDBconnect(“http://localhost: 1234”),会话=等待asyncGetSession(连接),用户=等待asyncGetUser(会话),日志=等待asyncLogAccess(用户);返回日志;}抓(e){控制台.日志(“错误”,犯错);返回零;}}//执行异步函数(异步()= >{等待连接();})();
等待
有效地使每个调用看起来好像是同步的,同时不会占用JavaScript的单个处理线程。此外,异步
函数总是返回一个Promise,这样它们就可以被其他函数调用异步
功能。
异步
/等待
代码可能不会更短,但有相当大的好处:
- 语法更清晰。括号更少,出错的几率也更小。
- 调试更容易。断点可以设置在任何
等待
声明。 - 错误处理更好。
试一试
/抓
块的使用方式与同步代码相同。 - 支持是件好事。它可以在所有浏览器(除了IE和Opera Mini)和Node 7.6+中实现。
也就是说,并非一切都是完美的……
承诺,承诺
异步
/等待
仍然依赖于promise,而promise最终依赖于回调。你需要理解承诺是如何工作的,没有直接的对等物Promise.all ()
而且Promise.race ()
.这很容易被忘记Promise.all ()
,这比使用一系列不相关的更有效等待
命令。
同步循环中的异步等待
在某些时候,您将尝试调用异步函数内部一个同步循环。例如:
异步函数过程(数组){为(让我的数组){等待doSomething(我);}}
这是行不通的。这也不行:
异步函数过程(数组){数组.forEach(异步我= >{等待doSomething(我);});}
循环本身保持同步,并且总是在它们内部的异步操作之前完成。
ES2018引入了异步迭代器,它就像常规迭代器一样,只是next ()
方法返回一个Promise。因此,等待
关键字可以使用with对于……
循环以串联方式运行异步操作。例如:
异步函数过程(数组){为等待(让我的数组){doSomething(我);}}
但是,在实现异步迭代器之前,最好是这样做地图
数组项到异步
函数并运行它们Promise.all ()
.例如:
常量待办事项=[“一个”,“b”,“c”],alltodo=待办事项.地图(异步(v,我)= >{控制台.日志(“迭代”,我);等待processSomething(v);});等待承诺.所有(alltodo);
这样做的好处是可以并行运行任务,但是不可能将一个迭代的结果传递给另一个迭代,而且映射大型数组的计算成本可能很高。
try / catch丑陋
异步
函数将以静默方式退出试一试
/抓
在任何等待
而失败。如果你有一个很长的异步集等待
命令,您可能需要多个试一试
/抓
块。
一种替代方法是高阶函数,它可以捕捉错误试一试
/抓
块变得没有必要了(多亏了@wesbos有关建议):
异步函数连接(){常量连接=等待asyncDBconnect(“http://localhost: 1234”),会话=等待asyncGetSession(连接),用户=等待asyncGetUser(会话),日志=等待asyncLogAccess(用户);返回真正的;}//捕获错误的高阶函数函数catchErrors(fn){返回函数(...arg游戏){返回fn(...arg游戏).抓(犯错= >{控制台.日志(“错误”,犯错);});}}(异步()= >{等待catchErrors(连接)();})();
但是,在应用程序必须以不同于其他错误的方式对某些错误作出反应的情况下,此选项可能不实用。
尽管有一些陷阱,异步
/等待
是JavaScript的优雅补充。更多资源:
JavaScript的旅程
异步编程是JavaScript中无法避免的挑战。回调在大多数应用程序中都是必不可少的,但它很容易陷入嵌套很深的函数中。
承诺抽象回调,但有许多语法陷阱。转换现有函数可能是一件苦差事那就是()
锁链看起来仍然很凌乱。
幸运的是,异步
/等待
提供清晰。代码看起来是同步的,但它不能独占单个处理线程。它将改变你编写JavaScript的方式,甚至会让你感激Promises——如果你以前没有!