理解 JavaScript 中的 Promise
AWS 安全直播!

我向你保证,看完这篇文章,你一定会对
JavaScript Promises 有更深入的了解。
我对 JavaScript 一直抱有一种“爱恨交加”的复杂情感。尽管如此,
JavaScript 始终令我着迷。
过去十年我一直从事 Java 和 PHP 的开发工作,相比之下,JavaScript 显得截然不同,却又格外吸引我。我
之前投入到 JavaScript 学习中的时间并不多,
最近一直在努力弥补这方面的不足。
Promises 是我接触到的第一个有趣的话题。我经常
听到人们说 Promises 能让你摆脱回调地狱。虽然这
可能是一个令人欣喜的附加效果,但 Promises 的功能远不止于此,以下是
我目前为止的发现。这篇
文章会比较长,如果你想高亮显示某些部分,可以使用我们的扩展程序:
http://bit.ly/highlights-extension
背景
当你第一次接触 JavaScript 时,可能会感到有些
沮丧。你会听到有人说 JavaScript 是同步
编程语言,而另一些人则认为它是异步的。你还会听到
阻塞代码、非阻塞代码、事件驱动设计模式、事件生命周期、
函数栈、事件队列、冒泡、polyfill、Babel、Angular、ReactJS、Vue.js
以及其他一大堆工具和库。别担心,你不是第一个。这种
情况还有一个专门的术语,叫做“JavaScript 疲劳”。这条推文
很好地概括了这一点。
如果你想了解更多关于 JavaScript 疲劳的信息,可以看看
这篇文章。这篇文章在 Hackernoon 上获得了 4.2 万个赞是有原因的 :)
JavaScript 是一种同步编程语言。但借助回调
函数,我们可以让它像异步编程语言一样工作。
对普通人的承诺
JavaScript 中的 Promise 与你在现实生活中做出的承诺非常相似。
所以,我们先来看看现实生活中的 Promise。
字典中对“承诺”的定义如下:
promise:名词:保证某人会做某事或某件事
会发生。
那么,当有人向你做出承诺时会发生什么呢?
- 承诺能让你确信某件事将会完成。至于承诺者是亲自去做,还是委托他人去做,这并不重要。承诺给你的是一种保证,你可以据此制定计划。
- 承诺要么遵守,要么违背。
- 当承诺得到履行时,你会期望从中获得一些回报。你可以利用承诺的成果来指导你接下来的行动或计划。
- 当承诺被违背时,你会想知道为什么做出承诺的人没能履行诺言。一旦你了解了原因并确认承诺已被违背,你就可以计划下一步该怎么做或如何处理此事。
- 做出承诺时,我们所拥有的仅仅是保证,无法立即采取行动。我们可以决定并制定在承诺兑现(因此我们有了预期结果)或违背(因此我们知道原因,从而可以制定应急预案)时需要采取的措施。
- 有可能你根本收不到做出承诺的人的回复。在这种情况下,你最好设定一个时间期限。比如说,如果做出承诺的人10天后没有回复,我会认为他遇到了什么问题,不会履行承诺。所以,即使15天后他回复了,你也无所谓了,因为你已经另作安排。
JavaScript 中的 Promise
一般来说,学习 JavaScript 时我总是阅读 MDN Web
Docs 上的文档。我认为在所有资源中,它们提供的细节最简洁明了。我阅读
了MDN Web
Docs
上的 Promises 页面 ,并尝试编写代码来掌握它。
理解 Promise 包含两个部分:Promise 的创建和
Promise 的处理。虽然我们的大部分代码通常都会处理
由其他库创建的 Promise,但全面理解 Promise 肯定会对我们有所帮助。 一旦你过了入门阶段,
理解“Promise 的创建”就同样重要了。
创造承诺
让我们来看一下创建新承诺的标志。
构造函数接受一个名为 `executor` 的函数。该executor函数
接受两个参数resolve,reject这两个参数本身也是函数。Promise
通常用于更轻松地处理异步操作或
阻塞代码,例如文件操作、API 调用、数据库调用、IO
调用等。这些异步操作的启动发生在 ` executorexecutor` 函数内部。如果异步操作成功,则Promise的创建者
会调用 `executor` 函数来返回预期结果 。同样,如果出现意外错误,则会 通过调用`executor` 函数来传递错误原因。resolvereject
既然我们已经知道如何做出承诺,那么为了便于
理解,让我们来做出一个简单的承诺。
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 会立即被解析,我们无法检查
它的初始状态。所以,让我们创建一个需要
一些时间才能解析的新 Promise。最简单的方法是使用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);
上面的代码创建了一个 Promise,它会在 10
秒后无条件地解析。因此,我们可以持续检查该 Promise 的状态,直到它被解析为止。
十秒钟过后,Promise 即被解析。`and`PromiseStatus和`or`PromiseValue都会相应更新。如您所见,我们更新了 `resolve`
函数,使其可以传递 JSON 对象而非简单的字符串。这
只是为了说明我们也可以在该resolve函数中传递其他值。

一个在 10 秒后解析的 Promise,返回一个 JSON 对象。
现在我们来看一个会被拒绝的承诺。
为此,我们只需对承诺 1 稍作修改。
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);
由于这将导致未经处理的拒绝,Chrome 浏览器会显示错误信息。
您可以暂时忽略它,我们稍后再处理。
拒绝承诺
我们可以看到,PromisePromiseStatus可以有三个不同的值。当 Promise 创建时,它将处于待处理状态,直到 Promise 变为已处理或已完成状态。当 Promise 处于待处理或已完成状态时,我们称该 Promise 为已完成。因此 ,Promise通常会从待 处理状态转换为已完成状态。pendingresolvedrejectedPromiseStatuspendingPromiseValueundefinedresolvedrejected.resolvedrejectedsettled.
现在我们了解了 Promise 的创建方式,接下来就可以看看如何使用或
处理 Promise 了。这与理解对象本身密不可分Promise
。
理解承诺对象
根据 MDN 文档
该
Promise对象表示异步操作的最终完成(或失败)
及其结果值。
Promise对象拥有静态方法,prototype methods静态方法Promise可以独立应用,而原型方法则prototype methods
需要应用于对象的实例Promise。记住,无论是
普通方法还是原型方法都返回一个值,Promise这能让你更容易
理解它们。
原型方法
我们先从prototype methods这三个方法开始。再次
强调一下,所有这些方法都可以应用于Promise对象实例,并且都会返回一个 Promise。
以下所有方法都会为 Promise 的不同状态转换分配处理程序。
正如我们之前看到的,当 PromisePromise创建时,它处于pending初始状态。当 Promise 被结算时,以下三个方法中的一个或多个将根据 其状态(已结算或未结算)
运行。fulfilledrejected
Promise.prototype.catch(onRejected)
Promise.prototype.then(onFulfilled, onRejected)
Promise.prototype.finally(onFinally)
.then下图展示了 `finally`和 ` reject` 方法的流程.catch。由于它们
返回一个 ` PromisePromise`,因此可以链式调用,图中也显示了这一点。
如果`finally` 是为 Promise 声明的,那么无论 Promise 是已完成还是已拒绝,只要有 Promise.finally状态,它就会执行。正如Konstantin Rouda指出的,`finally` 的支持有限 ,因此请在使用前进行检查。settled
来源:
https://mdn.mozillademos.org/files/15911/promises.png
这是一个小故事。你是一个学生,你问妈妈要一部
手机。她说:“我月底会给你买一部手机。”
让我们来看看如果 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"
);
});
输出结果为:
如果我们把这个值改成momsSavings200000,那么妈妈就可以给
儿子送礼物了。在这种情况下,输出结果将是:
让我们设身处地地站在这个库的使用者的角度来思考。我们模拟它的
输出和特性,以便观察如何有效地使用和捕捉它们。
由于.then可以同时赋值两者onFulfilled, onRejected handlers,与其
分别编写.then,.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);
}
);
但为了提高代码的可读性,我认为最好将它们分开。
为了确保所有示例都能在浏览器(尤其是 Chrome)中运行,
我保证代码示例中没有任何外部依赖项
。为了更好地理解后续内容,我们创建一个函数
,该函数返回一个 Promise 对象,该对象会被随机解析或拒绝,以便
我们测试各种场景。为了理解异步
函数的概念,我们还会在函数中引入一个随机延迟。由于我们
需要随机数,因此我们首先创建一个随机函数,该函数会返回一个
介于 x 和 y 之间的随机数。
function getRandomNumber(start = 1, end = 10) {
//works when both start,end are >=1 and end > start
return parseInt(Math.random() * end) % (end-start+1) + start;
}
让我们创建一个函数,它会返回一个 Promise。我们调用这个
函数promiseTRRARNOSG,它是另一个函数的别名promiseThatResolvesRandomlyAfterRandomNumnberOfSecondsGenerator。这个函数
会创建一个 Promise,它会在 2 到 10 秒之间的随机时间后被解析或拒绝。
为了使解析和拒绝随机化,我们会生成一个
1 到 10 之间的随机数。如果生成的随机数大于 5,则
解析 Promise,否则拒绝它。
function getRandomNumber(start = 1, end = 10) {
//works when both start and end are >=1
return (parseInt(Math.random() * end) % (end - start + 1)) + start;
}
var promiseTRRARNOSG = (
= 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);
});
// Let us loop through and create ten different promises using the function to see some variation. Some will be resolved and some will be rejected.
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 并检查它们的输出,而无需执行此操作。resolvereject
静态方法
对象中有四个静态方法Promise。
前两个是辅助方法或快捷方式,它们可以帮助你
轻松创建已解决或已拒绝的 Promise。
Promise.reject(reason)
帮助你创建一个被拒绝的承诺。
var promise3 = Promise.reject("Not interested");
promise3.then(function(value){
console.log("This will not run as it is a resolved 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(value)
帮助你做出坚定的承诺。
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);
});
另外,一个 Promise 可以有多个处理程序。因此,您可以将上面的
代码更新为:
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。当需要处理
多个 Promise 时,最好先创建一个 Promise 数组,然后
再对这个数组执行必要的操作。为了更好地理解这些
方法,我们不能直接使用随机生成的Promise promiseTRRARNOSG,因为它过于
随机。最好使用一些确定性的 Promise,这样我们才能
更好地理解其行为。让我们创建两个函数:一个在 n 秒后解析成功
,另一个在 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)方法返回一个单一的PromisePromise,当参数
中的所有 Promiseiterable都已解决或
可迭代参数中不包含任何 Promise 时,该 Promise 将被解析。如果参数被拒绝,则该方法返回第一个被拒绝的 Promise 的拒绝原因
。
情况一:所有承诺都已解决。这是最常用
的场景。
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);
});
所有承诺均已兑现。
从输出结果中,我们需要得出两个重要的观察结论。
首先:第三个 Promise 耗时 2 秒,比
耗时 4 秒的第二个 Promise 更早完成。但正如你在输出中看到的,
Promise 的顺序在值中得到了保持。
第二点:我添加了一个控制台计时器来测量执行时间Promise.All。
如果这些 Promise 是顺序执行的,那么
总共应该需要 1+4+2=7 秒。但从计时器来看,实际只用了 4 秒。这证明
所有 Promise 都是并行执行的。
**情况二:**当没有任何承诺时。我认为这种情况最少
使用。
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,因此返回的 Promise 会被解析。
**情况 3:**它以第一个被拒绝的承诺所为由拒绝。
console.time("Promise.All");
var promisesArray = [];
promisesArray.push(promiseTRSANSG(1));
promisesArray.push(promiseTRSANSG(5));
promisesArray.push(promiseTRSANSG(3));
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);
});
第一次拒绝后,执行停止。
承诺.竞赛
根据MDN文档
该
Promise.race(iterable)方法返回一个 Promise,该Promise 会
在可迭代对象中的一个 Promise 解决或拒绝时立即解决或拒绝,并返回
该 Promise 的值或原因。
情况 1:其中一个承诺先得到解决。
console.time("Promise.race");
var promisesArray = [];
promisesArray.push(promiseTRSANSG(4));
promisesArray.push(promiseTRSANSG(3));
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);
});
所有 Promise 都是并行执行的。第三个 Promise 会在 2 秒后解析。
一旦第三个 Promise 解析完成,返回的 PromisePromise.race也会立即被解析。
情况二:其中一位承诺者首先拒绝了对方。
console.time("Promise.race");
var promisesArray = [];
promisesArray.push(promiseTRSANSG(4));
promisesArray.push(promiseTRSANSG(6));
promisesArray.push(promiseTRSANSG(5));
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);
});
最快拒绝
所有 Promise 并行执行。第四个 Promise 在 3 秒后被拒绝。
一旦这一步完成,返回的 PromisePromise.race也会被拒绝。
我编写了所有示例方法,以便测试各种场景
,并且测试可以直接在浏览器中运行。这就是为什么示例中没有
API 调用、文件操作或数据库调用的原因。虽然这些都是
实际应用场景,但你需要额外花费精力来设置和测试它们。
而使用延迟函数则可以让你轻松实现类似的场景,无需
额外的设置。你可以轻松地调整这些值来查看和
测试不同的场景。你可以结合使用 `and`和promiseTRJANSG` methods` 来模拟足够多的场景,从而 深入理解 Promise。此外,在相关代码块前后使用`methods` 可以帮助我们轻松识别 Promise 是并行执行 还是顺序执行。如果你有其他有趣的场景,或者我遗漏了什么,请告诉我 。如果你想在一个地方查看所有代码示例, 请查看这个 gist。promiseTRSANSGpromiseTRRARNOSGconsole.time
Bluebird 有一些有趣的功能,例如
- Promise.prototype.timeout
- 承诺。一些
- 承诺。
我们将在另一篇文章中讨论这些问题。
我还会再写一篇关于我从 async 和 await 中学到的东西的文章。
在结束之前,我想列出我为了
在承诺方面保持理智而遵循的所有经验法则。
使用 Promise 的经验法则
- 在编写异步或阻塞代码时,请使用 Promise。
resolve在所有实际应用中,映射then和reject映射。catch- 请确保为所有 Promise编写 `and`
.catch和 ` methods` 方法。.then - 如果两种情况下都需要做某事,请使用
.finally - 我们只有一次机会来改变每一个承诺。
- 我们可以向同一个 Promise 添加多个处理程序。
- 对象中所有方法的返回类型,
Promise无论是静态方法还是原型方法,都是一个Promise - 承诺的顺序
Promise.all是保持不变的,无论哪个承诺最先得到解决。




