使用承诺

A类承诺是一个表示异步操作最终完成或失败的对象。由于大多数人都是已经创造的承诺的消费者,因此本指南将在解释如何创造承诺之前,先解释如何消费返回的承诺。

从本质上讲,promise是一个返回的对象,您可以向其附加回调,而不是将回调传递到函数中。想象一个函数,创建音频文件异步(),它异步生成一个给定配置记录的声音文件和两个回调函数:一个在音频文件创建成功时调用,另一个在出现错误时调用。

下面是一些使用的代码创建音频文件异步():

js型
函数成功回调(结果){console.log(`URL:${result}`处的音频文件就绪`);}函数failureCallback(错误){console.error(`生成音频文件时出错:${error}`);}createAudioFileAsync(音频设置,成功回调,失败回调);

如果创建音频文件异步()被重写以返回承诺,您可以将回叫附加到该承诺上:

js型
createAudioFileAsync(audioSettings).then(成功回调,失败回调);

这个约定有几个优点。我们将逐一探讨。

链传动

一个常见的需求是背靠背地执行两个或多个异步操作,其中每个后续操作都是在前一个操作成功时开始的,并且是前一步的结果。在过去,连续执行几个异步操作将导致经典的回调地狱:

js型
doSomething(函数(结果){doSomethingElse(结果,函数(newResult){doThirdThing(newResult,函数(finalResult){log(`得到最终结果:${finalResult}`);},failureCallback);},failureCallback);},failureCallback);

通过承诺,我们通过创建承诺链来实现这一点。promise的API设计使得这一点非常棒,因为回调被附加到返回的promise对象,而不是传递到函数中。

神奇之处在于:然后()函数返回重新允诺,与原件不同:

js型
const promise=doSomething();const promise2=promise.then(成功回调,失败回调);

第二个承诺(承诺2)代表的不仅仅是做某事(),但也属于成功回拨故障回拨您传入了-可以是其他返回承诺的异步函数。在这种情况下,任何回调都会添加到承诺2排队等待任何一方返回的承诺成功回拨故障回拨.

注:如果希望使用工作示例,可以使用以下模板创建任何返回承诺的函数:

js型
函数doSomething(){return new Promise((resolve)=>{setTimeout(()=>{//完成承诺前要做的其他事情console.log(“做了某事”);//承诺的履行价值解决(“https://example.com/");}, 200);});}

该实现在围绕旧回调API创建Promise第节。

使用此模式,您可以创建更长的处理链,其中每个承诺表示链中一个异步步骤的完成。此外然后是可选的,并且catch(故障回调)是的缩写然后(null,failureCallback)-因此,如果所有步骤的错误处理代码都相同,则可以将其附加到链的末端:

js型
做某事().then(函数(结果){return doSomethingElse(结果);}).then(函数(newResult){return doThirdThing(newResult);}).then(函数(finalResult){log(`得到最终结果:${finalResult}`);}).catch(failureCallback);

你可能会看到这一点用箭头功能相反:

js型
做某事().then((结果)=>doSomethingElse(结果)).then((newResult)=>doThirdThing(newRes结果)).then((finalResult)=>{log(`得到最终结果:${finalResult}`);}).catch(故障回调);

注:箭头函数表达式可以有隐性回报; 所以,()=>x是的缩写()=>{return x;}.

做些其他事情做第三件事可以返回任何值&如果它们返回承诺,则该承诺将首先等待,直到它完成,而下一个回调将接收实现值,而不是承诺本身。务必回报来自然后回调,即使承诺总是决定未定义。如果之前的处理者开始承诺但没有返回,则无法再跟踪其结算,并且承诺被称为“浮动”。

js型
做某事().then((url)=>{//fetch(url)前面缺少“return”关键字。获取(url);})然后(结果)=>{//结果未定义,因为上一个//处理程序。无法知道fetch()的返回值//再打电话,或者它是否成功。});

通过返回取来调用(这是一个承诺),我们既可以跟踪它的完成情况,也可以在它完成时接收它的值。

js型
做某事().then((url)=>{//添加了`return`关键字返回获取(url);})然后(结果)=>{//结果是一个Response对象});

如果您有竞态条件,浮动承诺可能会更糟-如果最后一个处理程序的承诺没有返回,则下一个处理函数的承诺然后处理程序将被提前调用,它读取的任何值都可能不完整。

js型
const listOfIngredients=[];做某事().then((url)=>{//fetch(url)前面缺少“return”关键字。获取(url).then((res)=>res.json())然后((数据)=>{listOfIndients.push(数据);});})然后(()=>{console.log(成分列表);//listOfIngredients将始终为[],因为获取请求尚未完成。});

因此,根据经验法则,每当您的操作遇到承诺时,请将其退回并推迟到下一次处理然后处理程序。

js型
const listOfIngredients=[];做某事().then((url)=>{//“return”关键字现在包含在fetch调用之前。返回获取(url).then((res)=>res.json())然后((数据)=>{listOfIndients.push(数据);});})然后(()=>{console.log(成分列表);//listOfIndients现在将包含来自fetch调用的数据。});

更好的是,您可以将嵌套链展平为单个链,这样更简单,也更容易处理错误。有关详细信息,请参阅嵌套第节。

js型
做某事().then((url)=>获取.then((res)=>res.json())然后((数据)=>{listOfIndients.push(数据);})然后(()=>{console.log(成分列表);});

使用异步/等待可以帮助您编写更直观且类似于同步代码的代码。下面是使用异步/等待:

js型
异步函数logIngredients(){const url=等待doSomething();const res=等待获取(url);const数据=等待res.json();listOfIngredients.push(数据);console.log(成分列表);}

请注意,除了等待承诺前的关键词。唯一的折衷之一是很容易忘记等待关键字,只有在类型不匹配时才能修复(例如,尝试使用promise作为值)。

异步/等待建立在承诺之上——例如,做某事()是与以前相同的函数,因此只需进行最少的重构即可将承诺更改为异步/等待。您可以阅读有关异步/等待中的语法异步函数等待参考文献。

注: 异步/等待与普通承诺链具有相同的并发语义。等待在一个异步函数中,不会停止整个程序,只会停止依赖于其值的部分,因此在等待正在挂起。

错误处理

你可能记得看到过故障回拨与承诺链末端只有一次相比,在厄运金字塔中提前了三次:

js型
做某事().then((结果)=>doSomethingElse(结果)).then((newResult)=>doThirdThing(newRes结果)).then((finalResult)=>console.log(`得到最终结果:${finalRes结果}`)).catch(failureCallback);

如果出现异常,浏览器将在链中查找.catch()处理程序或on拒绝。这在很大程度上模仿了同步代码的工作方式:

js型
尝试{const结果=syncDoSomething();const newResult=syncDoSomethingElse(结果);const finalResult=syncDoThirdThing(newResult);log(`得到最终结果:${finalResult}`);}捕获(错误){failureCallback(错误);}

异步代码的这种对称性在异步/等待语法:

js型
异步函数foo(){尝试{const result=等待doSomething();const newResult=等待doSomethingElse(result);const finalResult=等待doThirdThing(newResult);log(`得到最终结果:${finalResult}`);}捕获(错误){failureCallback(错误);}}

承诺通过捕获所有错误,甚至抛出的异常和编程错误,解决了回调金字塔的一个基本缺陷。这对于异步操作的功能组合至关重要。所有错误现在都由捕捉()方法,您几乎不需要使用尝试/抓住不使用异步/等待.

嵌套

在上述示例中,涉及成分清单,第一个承诺链嵌套在另一个承诺链的返回值中然后()处理程序,而第二个使用完全扁平的链条。简单的承诺链最好保持平坦,不要嵌套,因为嵌套可能是粗心组合的结果。

嵌套是一种控制结构,用于限制抓住声明。具体来说,嵌套抓住只捕获其范围及其以下的错误,而不捕获嵌套范围外链中较高级别的错误。如果正确使用,这将在错误恢复中提供更高的精度:

js型
做一些至关重要的事情()然后(结果)=>doSomething可选(结果).then((optionalResult)=>doSomethingExtraNice(optional Result,可选结果)).catch((e)=>{}),)//如果可选内容失败,则忽略;继续。.then(()=>moreCriticalStuff()).catch((e)=>console.error(`严重故障:${e.message}`));

注意,这里的可选步骤是嵌套的,嵌套不是由缩进引起的,而是由外部的放置引起的()步骤周围的括号。

内部错误-静音抓住处理程序仅捕获来自的故障doSomethingOptional()做一些额外的事情(),之后代码继续更多批评填充()重要的是,如果做某事关键()失败,其错误被final(外部)捕获抓住只有这样,才不会被内心吞噬抓住处理程序。

异步/等待,此代码如下所示:

js型
异步函数main(){尝试{const result=等待doSomethingCritical();尝试{const optionalResult=等待doSomethingOptional(结果);等待doSomethingExtraNice(可选结果);}捕捉(e){//忽略可选步骤中的失败并继续。}等待更多CriticalStuff();}捕捉(e){console.error(`严重故障:${e.message}`);}}

注:如果没有复杂的错误处理,则很可能不需要嵌套然后处理程序。相反,使用扁平链并将错误处理逻辑放在末尾。

接球后用链子锁住

可以用链条之后故障,即抓住,即使在链中的某个操作失败后,这对于完成新操作也很有用。阅读以下示例:

js型
做某事()然后(()=>{throw new Error(“Something failed”);console.log(“执行此操作”);}).catch(()=>{console.error(“执行该操作”);})然后(()=>{console.log(“无论之前发生了什么,都要这样做”);});

这将输出以下文本:

首字母这样做吧不管以前发生了什么,都要这样做

注:文本“Do this”未显示,因为“Something failed”错误导致拒绝。

异步/等待,此代码如下所示:

js型
异步函数main(){尝试{等待doSomething();throw new Error(“Something failed”);console.log(“Do this”);}捕捉(e){console.error(“执行该操作”);}console.log(“无论之前发生了什么,都要这样做”);}

拒绝承诺事件

如果任何处理程序都没有处理承诺拒绝事件,那么它会冒泡到调用堆栈的顶部,主机需要将其浮出水面。在web上,每当承诺被拒绝时,两个事件之一都会发送到全局范围(通常,这是窗口或者,如果在webworker中使用,它是工人或其他基于工作的界面)。这两个事件是:

未经处理的弹射

当承诺被拒绝但没有可用的拒绝处理程序时发送。

已处理拒绝

当处理程序附加到已导致未经处理的弹射事件。

在这两种情况下承诺拒绝事件)作为成员a承诺表示被拒绝的承诺的属性,以及原因提供拒绝承诺的原因的属性。

这样就可以为promise提供回退错误处理,并帮助调试promise管理中的问题。这些处理程序是每个上下文的全局处理程序,因此所有错误都将传递给相同的事件处理程序,而不管其来源如何。

节点.js,处理拒绝承诺略有不同。通过为Node.js添加处理程序,可以捕获未处理的拒绝未处理拒绝事件(注意名称大小写的不同),如下所示:

js型
process.on(“未处理拒绝”,(原因,承诺)=>{//在此处添加代码以检查“promise”和“reason”值});

对于Node.js,为了防止错误被记录到控制台(否则会发生的默认操作),添加了process.on()过程倾听者是一切必要的;没有必要使用与浏览器运行时等效的预防默认()方法。

然而,如果你加上进程.打开监听器中没有代码来处理被拒绝的承诺,它们只会被扔在地板上,被悄悄地忽略。因此,理想情况下,您应该在该侦听器中添加代码,以检查每个被拒绝的承诺,并确保它不是由实际的代码错误引起的。

组成

有四个合成工具并发运行异步操作:Promise.all(),承诺.全部解决(),承诺.any()、和诺言赛跑().

我们可以同时开始操作,并等待它们全部完成,如下所示:

js型
Promise.all([func1(),func2(),func3()]).then(([result1,result2,result3])=>{//使用result1、result2和result3});

如果阵列中的一个承诺被拒绝,Promise.all()立即拒绝返回的承诺并中止其他操作。这可能会导致意外的状态或行为。承诺.全部解决()是另一个合成工具,可确保在解析之前完成所有操作。

这些方法都是并发运行承诺的——一系列承诺是同时启动的,不会相互等待。使用一些聪明的JavaScript可以进行顺序合成:

js型
[函数1,函数2,函数3].reduce((p,f)=>p.then(f),Promise.resolve())然后((结果3)=>{/*使用结果3*/});

在这个例子中,我们减少一组异步函数向下延伸到承诺链。上述代码相当于:

js型
承诺.resolve().then(函数1).then(函数2).then(函数3).则((结果3)=>{/*使用结果3*/});

这可以成为一个可重用的组合函数,这在函数编程中很常见:

js型
const applyAsync=(acc,val)=>acc.then(val);常量合成异步=(…函数)=>(x) =>funcs.reduce(applyAsync,Promise.resolve(x));

这个composeAsync()函数接受任意数量的函数作为参数,并返回一个新函数,该函数接受要通过合成管道传递的初始值:

js型
const transformData=composeAsync(func1,func2,func3);const result3=transformData(数据);

使用async/await也可以更简洁地进行顺序合成:

js型
let结果;for([func1,func2,func3]的常数f){result=等待f(结果);}/*使用最后的结果(即结果3)*/

然而,在按顺序编写承诺之前,请考虑是否真的有必要-最好是同时运行承诺,这样它们就不会不必要地相互阻塞,除非一个承诺的执行取决于另一个的结果。

取消

承诺本身没有用于取消的一级协议,但您可以直接取消底层异步操作,通常使用中止控制器.

围绕旧回调API创建Promise

A类承诺可以使用它的建造师。这应该只用于包装旧API。

在理想情况下,所有异步函数都会返回承诺。不幸的是,一些API仍然期望以旧的方式传递成功和/或失败回调。最明显的例子是设置超时()功能:

js型
setTimeout(()=>saySomething(“10秒过去了”),10*1000);

将老式的回调和承诺混为一谈是有问题的。如果说点什么()失败或包含编程错误,则任何东西都无法捕获它。这是设置超时.

幸运的是我们可以打包设置超时在承诺中。最佳实践是将回调接收函数包装在尽可能低的级别,然后再也不要直接调用它们:

js型
const wait=(ms)=>new Promise((resolve)=>setTimeout(resolution,ms));等待(10*1000).then(()=>说点什么(“10秒”).catch(failureCallback);

承诺构造函数采用执行器函数,该函数允许我们手动解析或拒绝承诺。设置超时()并不是真的失败,在这个案例中我们忽略了拒绝。有关执行器函数如何工作的更多信息,请参阅承诺()参考。

时间安排

最后,我们将研究有关何时调用注册回调的更多技术细节。

担保

在基于回调的API中,何时以及如何调用回调取决于API实现者。例如,可以同步或异步调用回调:

js型
函数doSomething(回调){if(数学随机数()>0.5){回调();}其他{setTimeout(()=>回调(),1000);}}

上述设计受到强烈反对,因为它会导致所谓的“扎尔戈状态”。在设计异步API的上下文中,这意味着回调在某些情况下是同步调用的,但在其他情况下是异步调用的,这给调用方带来了歧义。有关更多背景信息,请参阅文章设计异步API,该术语首次在这里正式提出。这种API设计使副作用难以分析:

js型
设值=1;doSomething(()=>{值=2;});console.log(值);//1或2?

另一方面,承诺是控制反转-API实现者不控制何时调用回调。相反,维护回调队列和决定何时调用回调的工作委托给承诺实现,API用户和API开发人员自动获得强大的语义保证,包括:

  • 添加了回调然后()完成当前运行JavaScript事件循环的。
  • 即使添加了这些回调,也会调用它们之后承诺所代表的异步操作的成功或失败。
  • 可以通过调用添加多个回调然后()多次。它们将按照插入的顺序依次调用。

为了避免意外,将函数传递给然后()永远不会被同步调用,即使有一个已经解决的承诺:

js型
Promise.resolve().then(()=>console.log(2));console.log(1);//日志:1、2

传入的函数不是立即运行,而是放在微任务队列中,这意味着它稍后运行(仅在创建它的函数退出之后,以及当JavaScript执行堆栈为空时),就在控制返回到事件循环之前;即很快:

js型
const wait=(ms)=>new Promise((resolve)=>setTimeout(resolution,ms));wait(0).then(()=>console.log(4));承诺.resolve().then(()=>控制台.log(2)).then(()=>console.log(3));console.log(1);//1, 2, 3, 4

任务队列与微任务

承诺回调处理为微任务然而设置超时()回调作为任务队列处理。

js型
const promise=新promise((resolve,reject)=>{console.log(“Promise回调”);resolve();})然后(结果)=>{console.log(“Promise回调(.then)”);});setTimeout(()=>{log(“事件-停止循环:承诺(已实现)”,承诺);}, 0);console.log(“Promise(pending)”,Promise);

上面的代码将输出:

承诺回叫承诺(待定)承诺{<待定>}承诺回调(.then)事件-停止循环:承诺(已实现)承诺{<已实现>}

有关更多详细信息,请参阅任务与微任务.

当承诺和任务发生冲突时

如果您遇到承诺和任务(如事件或回调)以不可预测的顺序启动的情况,那么在有条件地创建承诺时,使用微任务检查状态或平衡您的承诺可能会对您有所帮助。

如果您认为微任务可能有助于解决此问题,请参阅微任务指南了解有关如何使用的更多信息队列微任务()将函数作为微任务排队。

另请参见