前言前端
异步流程控制,曾经也分享过不少次了,那今天早读文章又是分享这个主题,由前端早读课专栏做者@HetfieldJoe带来的《你不懂JS系列》翻译分享。ajax
正文从这开始~编程
若是你写过至关数量的JavaScript,这就不是什么秘密:异步编程是一种必须的技能。曾经管理异步的主要机制是函数回调。数组
然而,ES6增长了一种新特性:Promise,来帮助你解决仅使用回调来管理异步的重大缺陷。另外,咱们能够重温generator(前一章中提到的)来看看一种将二者组合的模式,它是JavaScript中异步流程控制编程向前迈出的重要一步。promise
Promises浏览器
让咱们辨明一些误解:Promise不是回调的替代品。Promise提供了一种可信的中介机制 —— 也就是,在你的调用代码和将要执行任务的异步代码之间 —— 来管理回调。并发
另外一种考虑Promise的方式是做为一种事件监听器,你能够在它上面注册监听一个通知你任务什么时候完成的事件。它是一个仅被触发一次的时间,但无论怎样能够被看做是一个事件。app
Promise能够被连接在一块儿,它们能够是一系列顺序的、异步完成的步骤。与all(..)方法(用经典的术语将,叫“门”)和race(..)方法(用经典的术语将,叫“闩”)这样的高级抽象一块儿,promise链能够提供一种异步流程控制的机制。异步
还有另一种概念化Promise的方式是,将它看做一个 将来值,一个与时间无关的值的容器。不管底层的值是不是最终值,这种容器均可以被一样地推理。观测一个Promise的解析会在这个值准备好的时候将它抽取出来。换言之,一个Promise被认为是一个同步函数返回值的异步版本。async
一个Promise只可能拥有两种解析结果:完成或拒绝,并带有一个可选的信号值。若是一个Promise被完成,这个最终值称为一个完成值。若是它被拒绝,这个最终值称为理由(也就是“拒绝的理由”)。Promise只可能被解析(完成或拒绝)一次。任何其余的完成或拒绝的尝试都会被简单地忽略,一旦一个Promise被解析,它就成为一个不可被改变的值(immutable)。
显然,有几种不一样的方式能够来考虑一个Promise是什么。没有一个角度就它自身来讲是彻底充分的,可是每个角度都提供了总体的一个方面。这其中的要点是,它们为仅使用回调的异步提供了一个重大的改进,也就是它们提供了顺序、可预测性、以及可信性。
建立与使用 Promises
要构建一个promise实例,可使用Promise(..)构造器:
Promise(..)构造器接收一个单独的函数(pr(..)),它被当即调用并以参数值的形式收到两个控制函数,一般被命名为resolve(..)和reject(..)。它们被这样使用:
若是你调用reject(..),promise就会被拒绝,并且若是有任何值被传入reject(..),它就会被设置为拒绝的理由。
若是你不使用参数值,或任何非promise值调用resolve(..),promise就会被完成。
若是你调用resolve(..)并传入另外一个promise,这个promise就会简单地采用 —— 要么当即要么最终地 —— 这个被传入的promise的状态(不是完成就是拒绝)。
这里是你一般如何使用一个promise来重构一个依赖于回调的函数调用。假定你始于使用一个ajax(..)工具,它期预期要调用一个错误优先风格的回调:
你能够将它转换为:
Promise拥有一个方法then(..),它接收一个或两个回调函数。第一个函数(若是存在的话)被看做是promise被成功地完成时要调用的处理器。第二个函数(若是存在的话)被看做是promise被明确拒绝时,或者任何错误/异常在解析的过程当中被捕捉到时要调用的处理器。
若是这两个参数值之一被省略或者不是一个合法的函数 —— 一般你会用null来代替 —— 那么一个占位用的默认等价物就会被使用。默认的成功回调将传递它的完成值,而默认的错误回调将传播它的拒绝理由。
调用then(null,handleRejection)的缩写是catch(handleRejection)。
then(..)和catch(..)二者都自动地构建并返回另外一个promise实例,它被连接在本来的promise上,接收本来的promise的解析结果 —— (实际被调用的)完成或拒绝处理器返回的任何值。考虑以下代码:
在这个代码段中,咱们要么从fulfilled(..)返回一个当即值,要么从rejected(..)返回一个当即值,而后在下一个事件周期中这个当即值被第二个then(..)的fulfilled(..)接收。若是咱们返回一个新的promise,那么这个新promise就会做为解析结果被归入与采用:
要注意的是,在第一个fulfilled(..)中的一个异常(或者promise拒绝)将 不会 致使第一个rejected(..)被调用,由于这个处理仅会应答第一个原始的promise的解析。取代它的是,第二个then(..)调用所针对的第二个promise,将会收到这个拒绝。
在上面的代码段中,咱们没有监听这个拒绝,这意味着它会为了将来的观察而被静静地保持下来。若是你永远不经过调用then(..)或catch(..)来观察它,那么它将会成为未处理的。有些浏览器的开发者控制台可能会探测到这些未处理的拒绝并报告它们,可是这不是有可靠保证的;你应当老是观察promise拒绝。
注意: 这只是Promise理论和行为的简要概览。要进行更加深刻的探索,参见本系列的 异步与性能 的第三章。
Thenables
Promise是Promise(..)构造器的纯粹实例。然而,还存在称为 thenable 的类promise对象,它一般能够与Promise机制协做。
任何带有then(..)函数的对象(或函数)都被认为是一个thenable。任何Promise机制能够接受与采用一个纯粹的promise的状态的地方,均可以处理一个thenable。
Thenable基本上是一个通常化的标签,标识着任何由除了Promise(..)构造器以外的其余系统建立的类promise值。从这个角度上讲,一个thenable没有一个纯粹的Promise那么可信。例如,考虑这个行为异常的thenable:
若是你收到这个thenable并使用th.then(..)将它连接,你可能会惊讶地发现你的完成处理器被反复地调用,而普通的Promise本应该仅仅被解析一次。
通常来讲,若是你从某些其余系统收到一个声称是promise或thenable的东西,你不该当盲目地相信它。在下一节中,咱们将会看到一个ES6 Promise的工具,它能够帮助解决信任的问题。
可是为了进一步理解这个问题的危险,让咱们考虑一下,在 任何 一段代码中的 任何 对象,只要曾经被定义为拥有一个称为then(..)的方法就都潜在地会被误认为是一个thenable —— 固然,若是和Promise一块儿使用的话 —— 不管这个东西是否有意与Promise风格的异步编码有一丝关联。
在ES6以前,对于称为then(..)的方法历来没有任何特别的保留措施,正如你能想象的那样,在Promise出如今雷达屏幕上以前就至少有那么几种状况,它已经被选择为方法的名称了。最有可能用错thenable的状况就是使用then(..)的异步库不是严格兼容Promise的 —— 在市面上有好几种。
这份重担将由你来肩负:防止那些将被误认为一个thenable的值被直接用于Promise机制。
Promise API
PromiseAPI还为处理Promise提供了一些静态方法。
Promise.resolve(..)建立一个被解析为传入的值的promise。让咱们将它的工做方式与更手动的方法比较一下:
p1和p2将拥有彻底相同的行为。使用一个promise进行解析也同样:
提示: Promise.resolve(..)就是前一节提出的thenable信任问题的解决方案。任何你还不肯定是一个可信promise的值 —— 它甚至多是一个当即值 —— 均可以经过传入Promise.resolve(..)来进行规范化。若是这个值已是一个可识别的promise或thenable,它的状态/解析结果将简单地被采用,将错误行为与你隔绝开。若是相反它是一个当即值,那么它将会被“包装”进一个纯粹的promise,以此将它的行为规范化为异步的。
Promise.reject(..)建立一个当即被拒绝的promise,与它的Promise(..)构造器对等品同样:
虽然resolve(..)和Promise.resolve(..)能够接收一个promise并采用它的状态/解析结果,可是reject(..)和Promise.reject(..)不会区分它们收到什么样的值。因此,若是你使用一个promise或thenable进行拒绝,这个promise/thenable自己将会被设置为拒绝的理由,而不是它底层的值。
Promise.all([ .. ])接收一个或多个值(例如,当即值,promise,thenable)的数组。它返回一个promise,这个promise会在全部的值完成时完成,或者在这些值中第一个被拒绝的值出现时被当即拒绝。
使用这些值/promises:
让咱们考虑一下使用这些值的组合,Promise.all([ .. ])如何工做:
Promise.all([ .. ])等待全部的值完成(或第一个拒绝),而Promise.race([ .. ])仅会等待第一个完成或拒绝。考虑以下代码:
警告: 虽然 Promise.all([])将会当即完成(没有任何值),可是 Promise.race([])将会被永远挂起。这是一个奇怪的不一致,我建议你应当永远不要使用空数组调用这些方法。
Generators + Promises
将一系列promise在一个链条中表达来表明你程序的异步流程控制是 可能 的。考虑如以下代码:
可是对于表达异步流程控制来讲有更好的选项,并且在代码风格上可能比长长的promise链更理想。咱们可使用在第三章中学到的generator来表达咱们的异步流程控制。
要识别一个重要的模式:一个generator能够yield出一个promise,而后这个promise可使用它的完成值来推动generator。
考虑前一个代码段,使用generator来表达:
从表面上看,这个代码段要比前一个promise链等价物要更繁冗。可是它提供了更加吸引人的 —— 并且重要的是,更加容易理解和阅读的 —— 看起来同步的代码风格(“return”值的=赋值操做,等等),对于try..catch错误处理能够跨越那些隐藏的异步边界使用来讲就更是这样。
为何咱们要与generator一块儿使用Promise?不用Promise进行异步generator编码固然是可能的。
Promise是一个可信的系统,它将普通的回调和thunk中发生的控制倒转(参见本系列的 异步与性能)反转回来。因此组合Promise的可信性与generator中代码的同步性有效地解决了回调的主要缺陷。另外,像Promise.all([ .. ])这样的工具是一个很是美好、干净的方式 —— 在一个generator的一个yield步骤中表达并发。
那么这种魔法是如何工做的?咱们须要一个能够运行咱们generator的 运行器(runner),接收一个被yield出来的promise并链接它,让它要么使用成功的完成推动generator,要么使用拒绝的理由向generator抛出异常。
许多具有异步能力的工具/库都有这样的“运行器”;例如,Q.spawn(..)和个人asynquence中的runner(..)插件。这里有一个独立的运行器来展现这种处理如何工做:
注意: 这个工具的更丰富注释的版本,参见本系列的 异步与性能。另外,由各类异步库提供的这种运行工具一般要比咱们在这里展现的东西更强大。例如,asynquence的runner(..)能够处理被yield的promise、序列、thunk、以及(非promise的)间接值,给你终极的灵活性。
因而如今运行早先代码段中的*main()就像这样容易:
实质上,在你程序中的任何拥有多于两个异步步骤的流程控制逻辑的地方,你就能够 并且应当 使用一个由运行工具驱动的promise-yielding generator来以一种同步的风格表达流程控制。这样作将产生更易于理解和维护的代码。
这种“让出一个promise推动generator”的模式将会如此常见和如此强大,以致于ES6以后的下一个版本的JavaScript几乎能够肯定将会引入一中新的函数类型,它无需运行工具就能够自动地执行。咱们将在第八章中讲解async function(正如它们指望被称呼的那样)。
复习
随着JavaScript在它被普遍采用过程当中的日益成熟与成长,异步编程愈加地成为关注的中心。对于这些异步任务来讲回调并不彻底够用,并且在更精巧的需求面前全面崩塌了。
可喜的是,ES6增长了Promise来解决回调的主要缺陷之一:在可预测的行为上缺少可信性。Promise表明一个潜在异步任务的将来完成值,跨越同步和异步的边界将行为进行了规范化。
可是,Promise与generator的组合才彻底揭示了这样作的好处:将咱们的异步流程控制代码从新安排,将难看的回调浆糊(也叫“地狱”)弱化并抽象出去。
目前,咱们能够在各类异步库的运行器的帮助下管理这些交互,可是JavaScript最终将会使用一种专门的独立语法来支持这种交互模式!