在JavaScript中使用Promises

我们会不时发布与JavaScript中使用诺言有关的一种或另一种材料。


为什么这么多的注意力都集中在承诺上? 我们认为,最重要的是,这项技术的需求量很大,而且很难理解。

因此,如果您想更好地理解承诺,我们将为您提供有关该主题的下一篇文章的翻译。 它的作者说,过去十年来他一直在开发Java和PHP,但是一直以来,他一直对JavaScript感兴趣。 最近,他决定认真从事JS,并承诺成为第一个引起他兴趣的主题。



我们认为,对于初学者来说,尽管他们使用了诺言,但他们对诺言的理解不够充分,这些材料将使他们感兴趣。 某个人以崭新的眼光看待JavaScript并想向他人解释自己的理解,而不是相信某些事情是每个人都可以理解且无需解释的故事,这将有助于初学者掌握JavaScript机制。

初学者眼中的JavaScript


任何开始使用JavaScript编写的人都可能会感到所谓的“不合适”。 有人说JS是一种同步编程语言,有人说它是异步的。 新手会听到有关阻塞主线程的代码,有关不阻塞主线程的代码,基于事件的设计模式,事件的生命周期,函数调用堆栈,事件队列及其上升,polyfills的信息。 他了解到有诸如Babel,Angular,React,Vue之类的东西,还有许多其他的库。 如果您刚刚在这样的“新手”中认出自己-不用担心。 您既不是第一个也不是最后一个。 甚至还有一个术语-所谓的“ JavaScript疲劳”。 卢卡斯·科斯塔(Lucas F Costa)恰当地谈到了这一话题:“ 当人们使用他们不需要的工具来解决自己没有的问题时,就可以观察到JavaScript疲劳 。”

但是,我们不要谈论悲伤的事情。 因此,JavaScript是一种同步编程语言,由于采用了回调机制,因此您可以使用与异步语言相同的方式来调用函数。

一个关于诺言的简单故事


“诺言”一词转化为“诺言”。 编程中的承诺对象(我们称为“承诺”)与人们在现实生活中彼此给予的通常承诺非常相似。 因此,让我们先谈谈这些承诺。

在Wikipedia上,您可以找到“ promise”一词的以下定义 :“义务,即某人同意履行某项任务,或者相反地,不做某事”。 在Ozhegov的字典中,“承诺”是“自愿做某事”。

那么我们对诺言了解多少呢?

  1. 一个承诺给您保证一定会做的事情。 究竟是谁做的并不重要:做出承诺的人或应做出承诺的人的请求而执行的其他人。 诺言使人对某事充满信心,基于这种信心,接受诺言的人可以制定一些计划。
  2. 一个诺言可以实现或不实现。
  3. 如果兑现了诺言,那么您将期望将来可以使用某些东西来执行任何行动或实施任何计划。
  4. 如果诺言没有兑现,那么您将想找出为什么兑现诺言的人无法兑现诺言。 在找出发生问题的原因并且可以确保未兑现诺言之后,您可以考虑下一步该怎么做,或如何应对这种情况。
  5. 在向您承诺了某些事情之后,您所拥有的就是某种保证。 您不能立即兑现承诺。 您可以自己确定如果兑现了诺言,您将需要做什么(因此,您将收到诺言);如果发现诺言被破坏,您将需要做什么(在这种情况下,您知道发生了什么的原因,因此您可以考虑采取行动的备用计划) )
  6. 做出承诺的人很可能会消失。 在这种情况下,将承诺与某种时间框架联系在一起很有用。 例如,如果给您诺言的那个人在10天内没有出现,您可以认为他遇到了一些问题并且违反了诺言。 结果,即使做出承诺的人在15天之内履行了承诺,也没有关系,因此您已经在执行替代计划,而不是依靠承诺。

现在转到JavaScript。

JavaScript承诺


我有一条规则:使用JavaScript时,我总是阅读MDN上的文档 。 在我看来,此资源在呈现方式的特异性和清晰度方面可与其他资源相比。 因此,在学习promise的同时,我熟悉了相关材料并尝试了代码以适应新的语法构造。

为了理解承诺,您需要处理两个主要问题。 首先是创造承诺。 第二个是处理承诺返回的结果。 尽管我们编写的大多数代码都旨在处理例如由某些库创建的诺言,但是对诺言的工作机制的全面理解无疑将是有用的。 另外,对于已经有一定经验的程序员来说,知道如何创建承诺与知道如何使用它们一样重要。

创建承诺


这是创建诺言的方式:

new Promise( /* executor */ function(resolve, reject) { ... } ); 

构造函数接受执行某些动作的函数,在这里我们称其为executor 。 这个函数有两个参数- resolvereject ,这,反过来,也设有。 承诺通常用于执行异步操作或可能阻塞主线程的代码,例如,一个处理文件,进行API调用,进行数据库查询,处理I / O等的线程。 此类异步操作的开始在executor函数中executor 。 如果异步操作成功完成,则通过调用resolve函数将返回来自promise的预期结果。 调用此函数的情况由承诺的创建者确定。 同样,发生错误时,通过调用reject函数返回有关发生的情况的信息。

现在,我们大致了解了如何创建承诺,我们将创建一个简单的承诺,以便更好地理解所有内容。

 var keepsHisWord; keepsHisWord = true; promise1 = new Promise(function(resolve, reject) { if (keepsHisWord) {   resolve("The man likes to keep his word"); } else {   reject("The man doesnt want to keep his word"); } }); console.log(promise1); 

以下是此代码将输出的内容:


Promise具有状态(PromiseStatus)和值(PromiseValue)

由于我们的诺言得以立即解决,因此我们无法调查其初始状态。 因此,让我们创建一个新的承诺,要解决该问题,需要一些时间。 最简单的方法是使用setTimeout函数。

 promise2 = new Promise(function(resolve, reject) { setTimeout(function() {   resolve({     message: "The man likes to keep his word",     code: "aManKeepsHisWord"   }); }, 10 * 1000); }); console.log(promise2); 

在此代码中,创建了一个承诺,该承诺肯定会在10秒内解决。 这使我们有机会查看未经授权的承诺的状态。


未解决的承诺

10秒过后,诺言便会解决。 因此, PromiseStatusPromiseValue都将相应地更新。 如您所见,在此示例中,我们更改了成功解析promise时调用的函数,现在它返回的不是普通字符串,而是一个对象。 这样做是为了证明使用resolve函数返回复杂数据结构的能力。


10秒后解决承诺并返回对象

现在让我们看看诺言,我们决定不允诺,而是拒绝。 为此,我们修改了第一个示例中已经使用的代码。

 keepsHisWord = false; promise3 = new Promise(function(resolve, reject) { if (keepsHisWord) {   resolve("The man likes to keep his word"); } else {   reject("The man doesn't want to keep his word"); } }); console.log(promise3); 

由于我们不处理拒绝承诺的情况,因此浏览器控制台中将显示错误消息(此处使用Google Chrome)。 我们将在下面讨论更多。


被拒绝的承诺

现在,在分析了所有三个示例之后,我们可以看到PromiseStatus可以出现三个不同的值: pending (待处理),已resolved (成功解决)和已rejected (拒绝)。 创建承诺后,在PromiseStatus中将待pending ,在PromiseValue中将undefined 。 这些值将一直保留到承诺被解决或被拒绝为止。 当承诺处于已resolved或已rejected状态时,称为已settled承诺。 这样的PROMIS从待机状态到它有两种状态的状态移动resolved ,无论是国家rejected

现在我们已经了解了如何创建承诺,接下来我们可以讨论如何处理承诺的回报。 为了弄清楚这一点,我们需要了解Promise对象的结构。

承诺对象


根据MDN文档, Promise对象是异步操作成功或失败的结果。

Promise对象具有该对象的静态方法和原型方法。 可以在不创建对象实例的情况下调用静态方法,并且要调用原型方法,则需要Promise对象的实例。 请记住,静态方法和常规方法都将返回Promise对象。 这使工作更加轻松。

▍承诺对象原型方法


让我们首先讨论Promise对象的原型方法。 有三种这样的方法。 不要忘记可以在Promise对象实例上调用这些方法,并且它们本身会返回Promise 。 由于有了所有这些方法,您可以分配处理程序以响应诺言状态的变化。 正如我们已经看到的,当创建一个承诺时,它处于pending状态。 当承诺转换为已resolved或已rejected状态时,将至少调用以下方法之一:

 Promise.prototype.catch(onRejected) Promise.prototype.then(onFulfilled, onRejected) Promise.prototype.finally(onFinally) 

下面是操作的承诺和事件导致调用方法的示意图.then.catch 。 由于这些方法返回Promise对象,因此可以将它们的调用链接在一起,这也反映在图中。 如果promise使用.finally方法,则在promise移至已settled状态时将被调用,而不管该promise是成功解决还是被拒绝。


承诺工作计划(图片来自此处

这是一个短篇小说。 您是一名学生,要求您的母亲给您买手机。 她说:“如果我们节省的费用超过电话的费用,我会为您购买。” 现在,用JavaScript讲述这个故事。

 var momsPromise = new Promise(function(resolve, reject) { momsSavings = 20000; priceOfPhone = 60000; if (momsSavings > priceOfPhone) {   resolve({     brand: "iphone",     model: "6s"   }); } else {   reject("We donot have enough savings. Let us save some more money."); } }); momsPromise.then(function(value) { console.log("Hurray I got this phone as a gift ", JSON.stringify(value)); }); momsPromise.catch(function(reason) { console.log("Mom coudn't buy me the phone because ", reason); }); momsPromise.finally(function() { console.log(   "Irrespecitve of whether my mom can buy me a phone or not, I still love her" ); }); 

以下是此代码将输出的内容:


妈妈没有答应

如果我们将变量momsSavings的值更改为200,000,那么妈妈将可以为儿子购买礼物。 在这种情况下,以上代码将输出以下内容。


妈妈信守诺言

现在,让我们假设所讨论的代码被设计为一个库,并且我们使用该库。 让我们谈谈.catch.catch的有效使用。

由于可以为onFulfilled方法分配成功解析了诺言时调用的onRejected处理程序和拒绝诺言时调用的onRejected处理程序,而不是同时使用.catch.catch方法,因此我们只需一个实现相同的效果.then方法。 可能是这样的:

 momsPromise.then( function(value) {   console.log("Hurray I got this phone as a gift ", JSON.stringify(value)); }, function(reason) {   console.log("Mom coudn't buy me the phone because ", reason); } ); 

这是一个有效的示例,但是为了防止代码可读性,最好使用.catch.catch方法,而不是一个通用的.catch

为了使这些示例在浏览器中运行,尤其是在Google Chrome中运行,我试图避免外部依赖。 为了更好地理解我们以后将要考虑的内容,让我们创建一个返回承诺的函数,该承诺的解决或拒绝是随机发生的。 这将使我们能够体验到兑现承诺的各种情况。 为了理解异步函数的功能,我们将在承诺中设置随机延迟。 由于我们需要随机数,因此我们创建了一个函数,该函数返回xy之间的随机数。 这是功能。

 function getRandomNumber(start = 1, end = 10) { //,      start  end >=1  end > start return parseInt(Math.random() * end) % (end-start+1) + start; } 

现在创建一个返回诺言的函数。 让我们把它promiseTRRARNOSG 。 此函数的名称表示promiseThatResolvesRandomlyAfterRandomNumnberOfSecondsGenerator ,即,它是promise的生成器,它们会在随机数秒后被随机解析或拒绝。 此功能将创建一个承诺,该承诺将在2到10秒之间的随机时间段后被允许或拒绝。 为了随机地允许或拒绝一个承诺,我们得到一个介于1和10之间的随机数。如果该数字大于5,则将允许该承诺,否则将被拒绝。

 function getRandomNumber(start = 1, end = 10) { //,      start  end >=1  end > start return (parseInt(Math.random() * end) % (end - start + 1)) + start; } var promiseTRRARNOSG = (promiseThatResolvesRandomlyAfterRandomNumnberOfSecondsGenerator = function() { return new Promise(function(resolve, reject) {   let randomNumberOfSeconds = getRandomNumber(2, 10);   setTimeout(function() {     let randomiseResolving = getRandomNumber(1, 10);     if (randomiseResolving > 5) {       resolve({         randomNumberOfSeconds: randomNumberOfSeconds,         randomiseResolving: randomiseResolving       });     } else {       reject({         randomNumberOfSeconds: randomNumberOfSeconds,         randomiseResolving: randomiseResolving       });     }   }, randomNumberOfSeconds * 1000); }); }); var testProimse = promiseTRRARNOSG(); testProimse.then(function(value) { console.log("Value when promise is resolved : ", value); }); testProimse.catch(function(reason) { console.log("Reason when promise is rejected : ", reason); }); //             ,    .     ,  - . for (i=1; i<=10; i++) { let promise = promiseTRRARNOSG(); promise.then(function(value) {   console.log("Value when promise is resolved : ", value); }); promise.catch(function(reason) {   console.log("Reason when promise is rejected : ", reason); }); } 

在浏览器控制台中运行此代码,以查看允许和拒绝的诺言的行为。 接下来,我们将讨论如何使用其他机制创建许多promise并检查其实现结果。

aPromise对象的静态方法


Promise对象有四种静态方法。

这是两种方法Promise.reject(reason)Promise.resolve(value) ,可让您分别创建拒绝和允许的承诺。

这是使用Promise.reject方法的方法,该方法创建被拒绝的Promise.reject

 var promise3 = Promise.reject("Not interested"); promise3.then(function(value){ console.log("This will not run as it is a rejected promise. The resolved value is ", value); }); promise3.catch(function(reason){ console.log("This run as it is a rejected promise. The reason is ", reason); }); 

这是一个使用Promise.resolve方法的示例,该方法创建成功的已解决的Promise.resolve

 var promise4 = Promise.resolve(1); promise4.then(function(value){ console.log("This will run as it is a resovled promise. The resolved value is ", value); }); promise4.catch(function(reason){ console.log("This will not run as it is a resolved promise", reason); }); 

应该注意的是,一个承诺可能有多个处理程序。 例如,根据前面的示例,您可以获得以下所示的代码。

 var promise4 = Promise.resolve(1); promise4.then(function(value){ console.log("This will run as it is a resovled promise. The resolved value is ", value); }); promise4.then(function(value){ console.log("This will also run as multiple handlers can be added. Printing twice the resolved value which is ", value * 2); }); promise4.catch(function(reason){ console.log("This will not run as it is a resolved promise", reason); }); 

这是它在浏览器控制台中显示的内容:


使用Promise时使用多个.then

以下两种方法Promise.allPromise.race旨在与Promise.race集一起使用。 如果为了解决某个问题而必须处理多个promise,将这些promise放置在一个数组中,然后对它们执行必要的操作是最方便的。 为了理解此处考虑的方法的本质,我们将无法使用方便的函数promiseTRRARNOSG ,因为其工作结果在很大程度上取决于案件的意愿。 对于我们而言,使用产生更可预测的承诺的东西将更加方便,这将使我们能够了解其行为。 因此,我们将创建两个新功能。 其中一个( promiseTRSANSG )将创建在n秒后解决的承诺,第二个( promiseTRJANSG )-在n秒后被拒绝的承诺。

 var promiseTRSANSG = (promiseThatResolvesAfterNSecondsGenerator = function( n = 0 ) { return new Promise(function(resolve, reject) {   setTimeout(function() {     resolve({       resolvedAfterNSeconds: n     });   }, n * 1000); }); }); var promiseTRJANSG = (promiseThatRejectsAfterNSecondsGenerator = function( n = 0 ) { return new Promise(function(resolve, reject) {   setTimeout(function() {     reject({       rejectedAfterNSeconds: n     });   }, n * 1000); }); }); 

现在,我们将使用这些功能来了解Promise.all方法的功能。

▍方法承诺


从MDN文档中,您可以发现Promise.all(iterable)方法返回的一个Promise.all(iterable)将在作为iterable参数传递的所有Promise.all(iterable) iterable或此参数不包含Promise.all(iterable)时被iterable 。 如果任何转让的承诺均被拒绝,则该承诺将被拒绝。
让我们看几个例子。

示例1


所有的承诺将在这里被允许。 这种情况是最常见的。

 console.time("Promise.All"); var promisesArray = []; promisesArray.push(promiseTRSANSG(1)); promisesArray.push(promiseTRSANSG(4)); promisesArray.push(promiseTRSANSG(2)); var handleAllPromises = Promise.all(promisesArray); handleAllPromises.then(function(values) { console.timeEnd("Promise.All"); console.log("All the promises are resolved", values); }); handleAllPromises.catch(function(reason) { console.log("One of the promises failed with the following reason", reason); }); 

以下是此代码将输出到控制台的内容:


允许的所有承诺

在分析了本示例的结果之后,可以得出两个重要的观察结果。

首先,第三个承诺的解析时间为2秒,在第二个承诺之前完成,但是,从代码生成的输出可以看出,承诺在数组中的顺序得以保留。

其次,该代码包含一个计时器,用于确定执行Promise.all指令所花费的时间。

如果promise是按顺序执行的,那么该指令的执行时间将为7秒(1 + 4 + 2)。 但是,计时器告诉我们,如果将结果四舍五入,整个操作将花费4秒钟。 这证明了所有诺言都是并行执行的。

示例2


现在考虑传递给Promise.all的数组中没有承诺的情况。 我相信这是此功能最不常用的用例。

 console.time("Promise.All"); var promisesArray = []; promisesArray.push(1); promisesArray.push(4); promisesArray.push(2); var handleAllPromises = Promise.all(promisesArray); handleAllPromises.then(function(values) { console.timeEnd("Promise.All"); console.log("All the promises are resolved", values); }); handleAllPromises.catch(function(reason) { console.log("One of the promises failed with the following reason", reason); }); 

这是此代码将生成的输出:


调用Promise.all并传递不包含任何对此方法的诺言的数组

由于数组中没有promise,因此Promise.all几乎可以立即Promise.all

示例3


现在让我们看看当传递给Promise.all的承诺Promise.all被拒绝时会发生什么。

 console.time("Promise.All"); var promisesArray = []; promisesArray.push(promiseTRSANSG(1)); promisesArray.push(promiseTRSANSG(5)); promisesArray.push(promiseTRSANSG(3)); promisesArray.push(promiseTRJANSG(2)); promisesArray.push(promiseTRSANSG(4)); var handleAllPromises = Promise.all(promisesArray); handleAllPromises.then(function(values) { console.timeEnd("Promise.All"); console.log("All the promises are resolved", values); }); handleAllPromises.catch(function(reason) { console.timeEnd("Promise.All"); console.log("One of the promises failed with the following reason ", reason); }); 

从下面显示的代码执行结果中可以看到, Promise.all执行在第一个被拒绝的诺言之后以及该诺言给出的消息输出之后停止。


在第一个被拒绝的承诺之后执行将停止

m Promise.race方法


MDN报告Promise.race(iterable)方法在分别允许或拒绝所传输的承诺之一之后,返回带有值或拒绝原因的允许或拒绝的承诺。

让我们Promise.race使用Promise.race示例。

示例1


它显示了传递给Promise.race的承诺之一在其他任何人面前得到解决时会发生什么。

 console.time("Promise.race"); var promisesArray = []; promisesArray.push(promiseTRSANSG(4)); promisesArray.push(promiseTRSANSG(3)); promisesArray.push(promiseTRSANSG(2)); promisesArray.push(promiseTRJANSG(3)); promisesArray.push(promiseTRSANSG(4)); var promisesRace = Promise.race(promisesArray); promisesRace.then(function(values) { console.timeEnd("Promise.race"); console.log("The fasted promise resolved", values); }); promisesRace.catch(function(reason) { console.timeEnd("Promise.race"); console.log("The fastest promise rejected with the following reason ", reason); }); 

这是执行此示例后进入控制台的内容。


Promis,解决速度比其他所有人快

这里的所有承诺都是并行执行的。 第三个承诺在2秒后解决。 一旦出现这种情况,PROMIS返回Promise.race ,已启用。

示例2


现在考虑当传递给Promise.race的承诺Promise.race被拒绝时的情况。

 console.time("Promise.race"); var promisesArray = []; promisesArray.push(promiseTRSANSG(4)); promisesArray.push(promiseTRSANSG(6)); promisesArray.push(promiseTRSANSG(5)); promisesArray.push(promiseTRJANSG(3)); promisesArray.push(promiseTRSANSG(4)); var promisesRace = Promise.race(promisesArray); promisesRace.then(function(values) { console.timeEnd("Promise.race"); console.log("The fasted promise resolved", values); }); promisesRace.catch(function(reason) { console.timeEnd("Promise.race"); console.log("The fastest promise rejected with the following reason ", reason); }); 

执行完此示例后,以下内容将进入控制台:


承诺在别人面前被拒绝

与前面的示例一样,此处的承诺是并行执行的。 第三个承诺在3秒后被拒绝。 一旦发生这种情况, Promise.race返回的Promise.race被拒绝。

一般示例和实验


我在一个地方收集了我们在本材料中考虑过的所有示例,这将使它们更容易进行试验,探索实现诺言的各种场景。 该代码旨在在浏览器中执行,因此在这里我们不使用任何API调用,不访问文件操作,也不使用数据库。 尽管所有这些都可以在实际项目的开发中找到应用,但我相信使用这些机制可以分散我们的主要目标-承诺。 使用模拟时间延迟的简单函数可以得到相似的结果,并且不会给我们带来更多细节。

通过亲自研究这些示例,您可以尝试使用代码,变量的值,并研究使用Promise的不同方案。 尤其是,您可以结合使用promiseTRJANSGpromiseTRSANSGpromiseTRRARNOSG ,以模拟许多使用promise的方案,这将使您更好地理解它们。 另外,请注意,使用console.time命令可让您找出执行特定代码段所需的时间,例如,找出是并行还是顺序执行promise。 这是带有代码的要点页面链接 。 顺便说一句,如果您愿意的话,请看一下Bluebird库,该库包含一些处理Promise的有趣方法。

总结


我为您提供了在使用诺言时正确遵守的规则列表。

  1. 当您使用异步或阻塞代码时,请使用promise。
  2. 要处理成功解决承诺的情况,请使用.catch方法;对于被拒绝的情况,请使用.catch
  3. 在所有promise中都使用.catch.catch
  4. - , , .finally .
  5. , , , .
  6. , .
  7. Promise , , , .
  8. Promise.all , .

, , , .

亲爱的读者们! , , ?

- ,
- 10% :



:)

Source: https://habr.com/ru/post/zh-CN418085/


All Articles