By: Nolan Lawson
作者:诺兰-劳森
Published: 18 May 2015
发布时间:2015 年 5 月 18 日
Fellow JavaScripters, it's time to admit it: we have a problem with promises.
JavaScript 的朋友们,是时候承认了:我们对承诺有意见。
No, not with promises themselves. Promises, as defined by the A+ spec, are awesome.
不,不是承诺本身。根据A+ 规范的定义,承诺是了不起的。
The big problem, which has revealed itself to me over the course of the past year, as I've watched numerous programmers struggle with the PouchDB API and other promise-heavy APIs, is this:
在过去的一年里,当我看到无数程序员在 PouchDB API 和其他承诺繁多的 API 中挣扎时,我发现了一个大问题:
Many of us are using promises without really understanding them.
我们中的许多人都在使用承诺,却没有真正理解它们。
If you find that hard to believe, consider this puzzle I recently posted to Twitter:
如果你觉得难以置信,不妨看看我最近在 Twitter 上发布的这个谜题:
Q: What is the difference between these four promises?
问:这四项承诺有何不同?
doSomething().then(function () {
return doSomethingElse();
});
doSomething().then(function () {
doSomethingElse();
});
doSomething().then(doSomethingElse());
doSomething().then(doSomethingElse);
If you know the answer, then congratulations: you're a promises ninja. You have my permission to stop reading this blog post.
如果你知道答案,那么恭喜你:你是一个承诺忍者。我允许你停止阅读这篇博文。
For the other 99.99% of you, you're in good company. Nobody who responded to my tweet could solve it, and I myself was surprised by the answer to #3. Yes, even though I wrote the quiz!
对于其他 99.99% 的人来说,你们是好伙伴。回复我推文的人都解不出来,而我自己也对 3 号题的答案感到惊讶。是的,尽管测验是我写的!
The answers are at the end of this post, but first, I'd like to explore why promises are so tricky in the first place, and why so many of us – novices and experts alike – get tripped up by them. I'm also going to offer what I consider to be the singular insight, the one weird trick, that makes promises a cinch to understand. And yes, I really do believe they're not so hard after that!
答案就在这篇文章的最后,但首先,我想探讨一下为什么承诺如此棘手,为什么我们这么多人--无论是新手还是专家--都会被承诺绊倒。我还将提出我认为能让人轻松理解承诺的独到见解和诡异诀窍。是的,我真的相信,在这之后,它们就不那么难了!
But to start with, let's challenge some common assumptions about promises.
但首先,让我们对一些关于承诺的常见假设提出质疑。
Wherefore promises? 为何许诺?
If you read the literature on promises, you'll often find references to the pyramid of doom, with some horrible callback-y code that steadily stretches toward the right side of the screen.
如果您阅读有关承诺的文献,您会发现其中经常提到 "厄运金字塔",即一些可怕的回调代码不断向屏幕右侧延伸。
Promises do indeed solve this problem, but it's about more than just indentation. As explained in the brilliant talk "Redemption from Callback Hell", the real problem with callbacks it that they deprive us of keywords like return
and throw
. Instead, our program's entire flow is based on side effects: one function incidentally calling another one.
Promises 确实解决了这个问题,但它所涉及的不仅仅是缩进。正如"回调地狱的救赎"(Redemption from Callback Hell)这一精彩演讲中所解释的,回调的真正问题在于它剥夺了我们使用return
和throw
等关键字的权利。相反,我们程序的整个流程都基于副作用:一个函数偶然调用另一个函数。
And in fact, callbacks do something even more sinister: they deprive us of the stack, which is something we usually take for granted in programming languages. Writing code without a stack is a lot like driving a car without a brake pedal: you don't realize how badly you need it, until you reach for it and it's not there.
事实上,回调做了一件更邪恶的事:它让我们失去了堆栈,而在编程语言中,堆栈是我们通常认为理所当然的东西。编写没有堆栈的代码就像驾驶一辆没有刹车踏板的汽车:你不会意识到你有多么需要它,直到你伸手去拿,它却不在那里。
The whole point of promises is to give us back the language fundamentals we lost when we went async: return
, throw
, and the stack. But you have to know how to use promises correctly in order to take advantage of them.
承诺的全部意义在于,让我们找回了在异步化过程中丢失的语言基本要素:返回
、抛出
和堆栈。但你必须知道如何正确使用承诺,才能充分利用它们。
Rookie mistakes 新手错误
Some people try to explain promises as a cartoon, or in a very noun-oriented way: "Oh, it's this thing you can pass around that represents an asynchronous value."
有些人试图用漫画或非常名词化的方式来解释承诺:"哦,它是一个可以传递的东西,代表一个异步值"。
I don't find such explanations very helpful. To me, promises are all about code structure and flow. So I think it's better to just go over some common mistakes and show how to fix them. I call these "rookie mistakes" in the sense of, "you're a rookie now, kid, but you'll be a pro soon."
我觉得这样的解释没什么帮助。对我来说,承诺就是代码结构和流程。因此,我认为最好只是复习一些常见错误,并说明如何解决它们。我称这些错误为 "菜鸟错误",意思是 "孩子,你现在是菜鸟,但很快就会成为专家"。
Quick digression: "promises" mean a lot of different things to different people, but for the purposes of this article, I'm only going to talk about the official spec, as exposed in modern browsers as window.Promise
. Not all browsers have window.Promise
though, so for a good polyfill, check out the cheekily-named Lie, which is about the smallest spec-compliant library out there.
快速离题:对不同的人来说,"承诺 "有很多不同的含义,但在本文中,我只讨论官方规范,即现代浏览器中的window.Promise
。不过,并非所有浏览器都有window.Promise
,所以要想获得一个好的多填充,请查看厚颜无耻的Lie,它是目前符合规范的最小库。
Rookie mistake #1: the promisey pyramid of doom
新手错误 1:一诺千金的厄运金字塔
Looking at how people use PouchDB, which has a largely promise-based API, I see a lot of poor promise patterns. The most common bad practice is this one:
PouchDB 的 API 主要基于许诺,在观察人们如何使用 PouchDB 的过程中,我发现了很多糟糕的许诺模式。最常见的不良模式就是这种:
remotedb.allDocs({
include_docs: true,
attachments: true
}).then(function (result) {
var docs = result.rows;
docs.forEach(function(element) {
localdb.put(element.doc).then(function(response) {
alert("Pulled doc with id " + element.doc._id + " and added to local db.");
}).catch(function (err) {
if (err.name == 'conflict') {
localdb.get(element.doc._id).then(function (resp) {
localdb.remove(resp._id, resp._rev).then(function (resp) {
// et cetera...
Yes, it turns out you can use promises as if they were callbacks, and yes, it's a lot like using a power sander to file your nails, but you can do it.
是的,事实证明你可以像使用回调一样使用承诺,是的,这就像使用电动砂光机锉指甲一样,但你可以做到。
And if you think this sort of mistake is only limited to absolute beginners, you'll be surprised to learn that I actually took the above code from the official BlackBerry developer blog! Old callback habits die hard. (And to the developer: sorry to pick on you, but your example is instructive.)
如果你认为只有初学者才会犯这种错误,那你一定会惊讶地发现,我上面的代码其实是取自黑莓手机开发者的官方博客!旧的回调习惯很难改掉(对开发者说:很抱歉找你的麻烦,但你的例子很有启发意义)。
A better style is this one:
更好的款式是这个:
remotedb.allDocs(...).then(function (resultOfAllDocs) {
return localdb.put(...);
}).then(function (resultOfPut) {
return localdb.get(...);
}).then(function (resultOfGet) {
return localdb.put(...);
}).catch(function (err) {
console.log(err);
});
This is called composing promises, and it's one of the great superpowers of promises. Each function will only be called when the previous promise has resolved, and it'll be called with that promise's output. More on that later.
这就是所谓的 "组合承诺"(composing promises),也是承诺的一大超级功能。每个函数只有在前一个许诺解决后才会被调用,而且调用时会使用该许诺的输出。稍后将详细介绍。
Rookie mistake #2: WTF, how do I use forEach()
with promises?
新手错误 2:WTF,如何在承诺中使用forEach()
?
This is where most people's understanding of promises starts to break down. As soon as they reach for their familiar forEach()
loop (or for
loop, or while
loop), they have no idea how to make it work with promises. So they write something like this:
大多数人对承诺的理解就是从这里开始崩溃的。一旦他们开始使用熟悉的forEach()
循环(或for
循环,或while
循环),他们就不知道如何让它与承诺一起工作。于是,他们写出了这样的代码
// I want to remove() all docs
db.allDocs({include_docs: true}).then(function (result) {
result.rows.forEach(function (row) {
db.remove(row.doc);
});
}).then(function () {
// I naively believe all docs have been removed() now!
});
What's the problem with this code? The problem is that the first function is actually returning undefined
, meaning that the second function isn't waiting for db.remove()
to be called on all the documents. In fact, it isn't waiting on anything, and can execute when any number of docs have been removed!
这段代码有什么问题?问题在于第一个函数实际上是返回未定义的
,这意味着第二个函数并没有等待db.remove()
在所有文档上被调用。事实上,它什么都不等,可以在任何数量的文档被删除后执行!
This is an especially insidious bug, because you may not notice anything is wrong, assuming PouchDB removes those documents fast enough for your UI to be updated. The bug may only pop up in the odd race conditions, or in certain browsers, at which point it will be nearly impossible to debug.
这是一个特别隐蔽的 bug,因为假设 PouchDB 删除文档的速度足够快,用户界面也能及时更新,那么你可能不会注意到任何问题。这个 bug 可能只会在奇特的竞赛条件下,或者在某些浏览器中出现,这时几乎不可能进行调试。
The TLDR of all this is that forEach()
/for
/while
are not the constructs you're looking for. You want Promise.all()
:
总而言之,forEach()
/for/while
并不是你要找的结构。你需要的是Promise.all():
db.allDocs({include_docs: true}).then(function (result) {
return Promise.all(result.rows.map(function (row) {
return db.remove(row.doc);
}));
}).then(function (arrayOfResults) {
// All docs have really been removed() now!
});
What's going on here? Basically Promise.all()
takes an array of promises as input, and then it gives you another promise that only resolves when every one of those other promises has resolved. It is the asynchronous equivalent of a for-loop.
这是怎么回事?基本上,Promise.all()
将一个承诺数组作为输入,然后给出另一个承诺,该承诺只有在所有其他承诺都已解析时才会解析。它相当于一个异步 for 循环。
Promise.all()
also passes an array of results to the next function, which can get very useful, for instance if you are trying to get()
multiple things from PouchDB. The all()
promise is also rejected if any one of its sub-promises are rejected, which is even more useful.Promise.all()
还会将一个结果数组传递给下一个函数,这非常有用,例如当你试图从 PouchDB获取
多个内容时。如果任何一个子承诺被拒绝,all()
承诺也会被拒绝,这就更有用了。
Rookie mistake #3: forgetting to add .catch()
新手错误 3:忘记添加 .catch()
This is another common mistake. Blissfully confident that their promises could never possibly throw an error, many developers forget to add a .catch()
anywhere in their code. Unfortunately this means that any thrown errors will be swallowed, and you won't even see them in your console. This can be a real pain to debug.
这是另一个常见错误。许多开发人员认为自己的承诺不可能抛出错误,因此忘记在代码中的任何地方添加.catch()
。不幸的是,这意味着任何抛出的错误都会被吞没,甚至无法在控制台中看到。这在调试时可能会非常麻烦。
To avoid this nasty scenario, I've gotten into the habit of simply adding the following code to my promise chains:
为了避免这种糟糕的情况,我已经养成了在承诺链中添加以下代码的习惯:
somePromise().then(function () {
return anotherPromise();
}).then(function () {
return yetAnotherPromise();
}).catch(console.log.bind(console)); // <-- this is badass
Even if you never expect an error, it's always prudent to add a catch()
. It'll make your life easier, if your assumptions ever turn out to be wrong.
即使你从未预料到会出错,但添加一个catch()
总是明智的。如果你的假设被证明是错误的,它会让你的生活更轻松。
Rookie mistake #4: using "deferred"
新手错误 4:使用 "延迟"
This is a mistake I see all the time, and I'm reluctant to even repeat it here, for fear that, like Beetlejuice, merely invoking its name will summon more instances of it.
这是我经常看到的一个错误,我甚至不愿意在这里重复,因为我担心,就像 "甲壳虫汁 "一样,只要提到它的名字,就会有更多的例子出现。
In short, promises have a long and storied history, and it took the JavaScript community a long time to get them right. In the early days, jQuery and Angular were using this "deferred" pattern all over the place, which has now been replaced with the ES6 Promise spec, as implemented by "good" libraries like Q, When, RSVP, Bluebird, Lie, and others.
简而言之,Promise 的历史源远流长,JavaScript 社区花了很长时间才将其完善。早期,jQuery 和 Angular 到处使用这种 "延迟 "模式,现在 ES6 Promise 规范已经取代了这种模式,Q、When、RSVP、Bluebird、Lie 等 "优秀 "库都实现了这种模式。
So if you are writing that word in your code (I won't repeat it a third time!), you are doing something wrong. Here's how to avoid it.
所以,如果你在代码中写了这个词(我不会重复第三遍!),你就做错了。下面是如何避免的方法。
First off, most promise libraries give you a way to "import" promises from third-party libraries. For instance, Angular's $q
module allows you to wrap non-$q
promises using $q.when()
. So Angular users can wrap PouchDB promises this way:
首先,大多数承诺库都提供了从第三方库 "导入 "承诺的方法。例如,Angular 的$q
模块允许您使用 $q.when()
封装非
$q 承诺。因此,Angular 用户可以通过这种方式封装 PouchDB 承诺:
$q.when(db.put(doc)).then(/* ... */); // <-- this is all the code you need
Another strategy is to use the revealing constructor pattern, which is useful for wrapping non-promise APIs. For instance, to wrap a callback-based API like Node's fs.readFile()
, you can simply do:
另一种策略是使用揭示构造器模式,这对于封装非承诺 API 非常有用。例如,要封装像 Node 的fs.readFile()
这样基于回调的 API,只需执行以下操作即可:
new Promise(function (resolve, reject) {
fs.readFile('myfile.txt', function (err, file) {
if (err) {
return reject(err);
}
resolve(file);
});
}).then(/* ... */)
Done! We have defeated the dreaded def... Aha, caught myself. :)
完成!我们打败了可怕的防御系统...啊哈,被我逮到了。)
For more about why this is an anti-pattern, check out the Bluebird wiki page on promise anti-patterns.
如需进一步了解为什么这是一种反模式,请查看Bluebird 维基百科中有关承诺反模式的页面。
Rookie mistake #5: using side effects instead of returning
新手错误 5:使用副作用而不是返回值
What's wrong with this code?
这段代码有什么问题?
somePromise().then(function () {
someOtherPromise();
}).then(function () {
// Gee, I hope someOtherPromise() has resolved!
// Spoiler alert: it hasn't.
});
Okay, this is a good point to talk about everything you ever need to know about promises.
好了,现在可以谈谈你需要了解的关于承诺的一切了。
Seriously, this is the one weird trick that, once you understand it, will prevent all of the errors I've been talking about. You ready?
说真的,这就是一个奇怪的窍门,一旦你明白了它,就能避免我刚才说的所有错误。准备好了吗?
As I said before, the magic of promises is that they give us back our precious return
and throw
. But what does this actually look like in practice?
正如我之前所说,承诺的魔力在于它能让我们重新获得宝贵的回报
和投掷
。但实际情况又是怎样的呢?
Every promise gives you a then()
method (or catch()
, which is just sugar for then(null, ...)
). Here we are inside of a then()
function:
每个承诺都会提供一个then()
方法(或catch
(
)
,后者只是then(null, ...)
的缩写)。我们现在就在then()
函数的内部:
somePromise().then(function () {
// I'm inside a then() function!
});
What can we do here? There are three things:
我们在这里能做什么?有三件事:
return
another promise返回
另一个允诺return
a synchronous value (orundefined
)返回
同步值(或未定义
值)throw
a synchronous error抛出
同步错误
That's it. Once you understand this trick, you understand promises. So let's go through each point one at a time.
就是这样。一旦你明白了这个窍门,你就明白了承诺。下面,让我们逐一讲解。
1. Return another promise
1.返回另一个承诺
This is a common pattern you see in the promise literature, as in the "composing promises" example above:
这是允诺文献中常见的模式,如上面的 "组成允诺 "示例:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// I got a user account!
});
Notice that I'm return
ing the second promise – that return
is crucial. If I didn't say return
, then the getUserAccountById()
would actually be a side effect, and the next function would receive undefined
instead of the userAccount
.
请注意,我正在返回
第二个承诺--这个返回
是至关重要的。如果我不说return
,那么getUserAccountById()
实际上就是一个副作用,下一个函数收到的将是undefined
而不是userAccount
。
2. Return a synchronous value (or undefined)
2.返回同步值(或未定义值)
Returning undefined
is often a mistake, but returning a synchronous value is actually an awesome way to convert synchronous code into promisey code. For instance, let's say we have an in-memory cache of users. We can do:
返回未定义
值通常是个错误,但返回同步值实际上是将同步代码转换为承诺代码的绝佳方法。例如,假设我们有一个用户内存缓存。我们可以这样做
getUserByName('nolan').then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
});
Isn't that awesome? The second function doesn't care whether the userAccount
was fetched synchronously or asynchronously, and the first function is free to return either a synchronous or asynchronous value.
是不是很棒?第二个函数并不关心userAccount
是同步还是异步获取的,而第一个函数可以自由返回同步或异步值。
Unfortunately, there's the inconvenient fact that non-returning functions in JavaScript technically return undefined
, which means it's easy to accidentally introduce side effects when you meant to return something.
不幸的是,JavaScript 中的非返回函数在技术上会返回未定义的
内容,这意味着当你想返回某些内容时,很容易意外地引入副作用。
For this reason, I make it a personal habit to always return or throw from inside a then()
function. I'd recommend you do the same.
因此,我个人的习惯是总是从then()
函数内部返回或抛出。我建议你也这样做。
3. Throw a synchronous error
3.抛出同步错误
Speaking of throw
, this is where promises can get even more awesome. Let's say we want to throw
a synchronous error in case the user is logged out. It's quite easy:
说到抛出
,这是承诺变得更棒的地方。假设我们想在用户注销时抛出
一个同步错误。这很简单:
getUserByName('nolan').then(function (user) {
if (user.isLoggedOut()) {
throw new Error('user logged out!'); // throwing a synchronous error!
}
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
}).catch(function (err) {
// Boo, I got an error!
});
Our catch()
will receive a synchronous error if the user is logged out, and it will receive an asynchronous error if any of the promises are rejected. Again, the function doesn't care whether the error it gets is synchronous or asynchronous.
如果用户已注销,我们的catch()
将收到同步错误;如果任何承诺被拒绝,它将收到异步错误。同样,函数并不关心收到的错误是同步错误还是异步错误。
This is especially useful because it can help identify coding errors during development. For instance, if at any point inside of a then()
function, we do a JSON.parse()
, it might throw a synchronous error if the JSON is invalid. With callbacks, that error would get swallowed, but with promises, we can simply handle it inside our catch()
function.
这一点尤其有用,因为它有助于在开发过程中识别编码错误。例如,如果我们在then()
函数内部的任何一点执行JSON.parse
()
,如果 JSON 无效,可能会抛出同步错误。如果使用回调,该错误就会被吞没,但如果使用承诺,我们就可以在catch()
函数中简单地处理该错误。
Advanced mistakes 高级错误
Okay, now that you've learned the single trick that makes promises dead-easy, let's talk about the edge cases. Because of course, there are always edge cases.
好了,既然你已经学会了让承诺变得轻而易举的唯一诀窍,那我们就来谈谈边缘情况吧。当然,边缘情况总是存在的。
These mistakes I'd classify as "advanced," because I've only seen them made by programmers who are already fairly adept with promises. But we're going to need to discuss them, if we want to be able to solve the puzzle I posed at the beginning of this post.
我把这些错误归类为 "高级 "错误,因为我只见过那些对承诺已经相当熟练的程序员犯过这些错误。但是,如果我们想解决我在本文开头提出的问题,我们就需要讨论这些错误。
Advanced mistake #1: not knowing about Promise.resolve()
高级错误 1:不了解Promise.resolve()
As I showed above, promises are very useful for wrapping synchronous code as asynchronous code. However, if you find yourself typing this a lot:
如上所述,承诺对于将同步代码封装为异步代码非常有用。不过,如果你发现自己经常键入以下代码
new Promise(function (resolve, reject) {
resolve(someSynchronousValue);
}).then(/* ... */);
You can express this more succinctly using Promise.resolve()
:
您可以使用Promise.resolve()
更简洁地表达这一点:
Promise.resolve(someSynchronousValue).then(/* ... */);
This is also incredibly useful for catching any synchronous errors. It's so useful, that I've gotten in the habit of beginning nearly all of my promise-returning API methods like this:
这对于捕获任何同步错误也非常有用。它非常有用,以至于我已经养成了这样的习惯,几乎我所有的承诺返回 API 方法都是这样开始的:
function somePromiseAPI() {
return Promise.resolve().then(function () {
doSomethingThatMayThrow();
return 'foo';
}).then(/* ... */);
}
Just remember: any code that might throw
synchronously is a good candidate for a nearly-impossible-to-debug swallowed error somewhere down the line. But if you wrap everything in Promise.resolve()
, then you can always be sure to catch()
it later.
请记住:任何可能同步抛出
的代码都有可能在某处出现几乎无法调试的吞并错误。但如果用Promise.resolve()
封装一切,就能确保稍后再捕获(
)
它。
Similarly, there is a Promise.reject()
that you can use to return a promise that is immediately rejected:
同样,您也可以使用Promise.reject()
返回一个被立即拒绝的承诺:
Promise.reject(new Error('some awful error'));
Advanced mistake #2: then(resolveHandler).catch(rejectHandler)
isn't exactly the same as then(resolveHandler, rejectHandler)
高级错误 2: then(resolveHandler).catch(rejectHandler)
与 then(resolveHandler, rejectHandler)
I said above that catch()
is just sugar. So these two snippets are equivalent:
我在上面说过,catch()
只是糖而已。所以这两个片段是等价的:
somePromise().catch(function (err) {
// handle error
});
somePromise().then(null, function (err) {
// handle error
});
However, that doesn't mean that the following two snippets are equivalent:
但是,这并不意味着以下两个片段是等价的:
somePromise().then(function () {
return someOtherPromise();
}).catch(function (err) {
// handle error
});
somePromise().then(function () {
return someOtherPromise();
}, function (err) {
// handle error
});
If you're wondering why they're not equivalent, consider what happens if the first function throws an error:
如果你想知道为什么它们不是等价的,请考虑一下如果第一个函数出错会发生什么:
somePromise().then(function () {
throw new Error('oh noes');
}).catch(function (err) {
// I caught your error! :)
});
somePromise().then(function () {
throw new Error('oh noes');
}, function (err) {
// I didn't catch your error! :(
});
As it turns out, when you use the then(resolveHandler, rejectHandler)
format, the rejectHandler
won't actually catch an error if it's thrown by the resolveHandler
itself.
事实证明,当你使用 then(resolveHandler, rejectHandler)
格式时,如果错误是由resolveHandler
自己抛出的,那么rejectHandler
实际上不会捕捉到错误。
For this reason, I've made it a personal habit to never use the second argument to then()
, and to always prefer catch()
. The exception is when I'm writing asynchronous Mocha tests, where I might write a test to ensure that an error is thrown:
因此,我养成了一个个人习惯,那
就是从不使用then() 的
第二个参数,而总是选择catch
()
。例外情况是在编写异步Mocha测试时,我可能会编写一个测试来确保错误被抛出:
it('should throw an error', function () {
return doSomethingThatThrows().then(function () {
throw new Error('I expected an error!');
}, function (err) {
should.exist(err);
});
});
Speaking of which, Mocha and Chai are a lovely combination for testing promise APIs. The pouchdb-plugin-seed project has some sample tests that can get you started.
说到这一点,Mocha和Chai是测试承诺 API 的绝佳组合。pouchdb-plugin-seed项目中有一些示例测试,可以帮助你开始测试。
Advanced mistake #3: promises vs promise factories
高级错误 3:承诺与承诺工厂
Let's say you want to execute a series of promises one after the other, in a sequence. That is, you want something like Promise.all()
, but which doesn't execute the promises in parallel.
比方说,您想按顺序一个接一个地执行一系列承诺。也就是说,您想要类似Promise.all()
的东西,但它不会并行执行承诺。
You might naïvely write something like this:
你可能会天真地写下这样的文字:
function executeSequentially(promises) {
var result = Promise.resolve();
promises.forEach(function (promise) {
result = result.then(promise);
});
return result;
}
Unfortunately, this will not work the way you intended. The promises you pass in to executeSequentially()
will still execute in parallel.
不幸的是,这不会按照您的预期运行。您传入executeSequentially()
的承诺仍将并行执行。
The reason this happens is that you don't want to operate over an array of promises at all. Per the promise spec, as soon as a promise is created, it begins executing. So what you really want is an array of promise factories:
出现这种情况的原因是你根本不想在承诺数组上操作。根据 promise 规范,一旦创建了一个 promise,它就会开始执行。所以你真正想要的是一个承诺工厂数组:
function executeSequentially(promiseFactories) {
var result = Promise.resolve();
promiseFactories.forEach(function (promiseFactory) {
result = result.then(promiseFactory);
});
return result;
}
I know what you're thinking: "Who the hell is this Java programmer, and why is he talking about factories?" A promise factory is very simple, though – it's just a function that returns a promise:
我知道你在想什么:"这个 Java 程序员到底是谁,他为什么要谈论工厂?许诺工厂非常简单,它只是一个返回许诺的函数:
function myPromiseFactory() {
return somethingThatCreatesAPromise();
}
Why does this work? It works because a promise factory doesn't create the promise until it's asked to. It works the same way as a then
function – in fact, it's the same thing!
为什么会这样呢?因为在被要求之前,承诺工厂不会创建承诺。它的工作方式与then
函数相同,实际上是一回事!
If you look at the executeSequentially()
function above, and then imagine myPromiseFactory
being substituted inside of result.then(...)
, then hopefully a light bulb will click in your brain. At that moment, you will have achieved promise enlightenment.
如果你看一下上面的executeSequentially()
函数,然后想象一下myPromiseFactory
被替换到result.then(...)
中,那么希望你的大脑中会有一个电灯泡。在那一刻,你将恍然大悟。
Advanced mistake #4: okay, what if I want the result of two promises?
高级错误 4:好吧,如果我想要两个承诺的结果呢?
Often times, one promise will depend on another, but we'll want the output of both promises. For instance:
很多时候,一个承诺依赖于另一个承诺,但我们需要两个承诺的输出结果。例如
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// dangit, I need the "user" object too!
});
Wanting to be good JavaScript developers and avoid the pyramid of doom, we might just store the user
object in a higher-scoped variable:
为了成为一名优秀的 JavaScript 开发人员,避免金字塔式的厄运,我们可能会将用户
对象存储在一个更高作用域的变量中:
var user;
getUserByName('nolan').then(function (result) {
user = result;
return getUserAccountById(user.id);
}).then(function (userAccount) {
// okay, I have both the "user" and the "userAccount"
});
This works, but I personally find it a bit kludgey. My recommended strategy: just let go of your preconceptions and embrace the pyramid:
这种方法可行,但我个人觉得有点笨拙。我建议的策略是:放下成见,拥抱金字塔:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id).then(function (userAccount) {
// okay, I have both the "user" and the "userAccount"
});
});
...at least, temporarily. If the indentation ever becomes an issue, then you can do what JavaScript developers have been doing since time immemorial, and extract the function into a named function:
......至少是暂时的。如果缩进成为一个问题,那么你可以像 JavaScript 开发人员自古以来一直在做的那样,将函数提取到一个命名函数中:
function onGetUserAndUserAccount(user, userAccount) {
return doSomething(user, userAccount);
}
function onGetUser(user) {
return getUserAccountById(user.id).then(function (userAccount) {
return onGetUserAndUserAccount(user, userAccount);
});
}
getUserByName('nolan')
.then(onGetUser)
.then(function () {
// at this point, doSomething() is done, and we are back to indentation 0
});
As your promise code starts to get more complex, you may find yourself extracting more and more functions into named functions. I find this leads to very aesthetically-pleasing code, which might look like this:
随着 promise 代码开始变得越来越复杂,您可能会发现自己将越来越多的函数提取到命名函数中。我发现这会带来非常美观的代码,可能看起来像这样:
putYourRightFootIn()
.then(putYourRightFootOut)
.then(putYourRightFootIn)
.then(shakeItAllAbout);
That's what promises are all about.
这就是承诺的意义所在。
Advanced mistake #5: promises fall through
高级错误 5:承诺落空
Finally, this is the mistake I alluded to when I introduced the promise puzzle above. This is a very esoteric use case, and it may never come up in your code, but it certainly surprised me.
最后,这就是我在上文介绍 promise 谜题时暗指的错误。这是一个非常深奥的用例,在你的代码中可能永远不会出现,但它确实让我大吃一惊。
What do you think this code prints out?
你认为这段代码会打印出什么?
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
console.log(result);
});
If you think it prints out bar
, you're mistaken. It actually prints out foo
!
如果你认为它会打印出bar
,那就错了。它实际上打印的是foo
!
The reason this happens is because when you pass then()
a non-function (such as a promise), it actually interprets it as then(null)
, which causes the previous promise's result to fall through. You can test this yourself:
出现这种情况的原因是,当您将then()
传递给一个非函数(如一个约定)时,它实际上会将其解释为then(null)
,从而导致前一个约定的结果落空。您可以亲自测试一下:
Promise.resolve('foo').then(null).then(function (result) {
console.log(result);
});
Add as many then(null)
s as you want; it will still print foo
.
您可以添加任意多的then(null)
;它仍然会打印foo
。
This actually circles back to the previous point I made about promises vs promise factories. In short, you can pass a promise directly into a then()
method, but it won't do what you think it's doing. then()
is supposed to take a function, so most likely you meant to do:
这实际上又回到了我之前提到的承诺与承诺工厂的问题上。简而言之,你可以直接将 promise 传递到then(
)
方法中,但它不会做你认为它要做的事情:
Promise.resolve('foo').then(function () {
return Promise.resolve('bar');
}).then(function (result) {
console.log(result);
});
This will print bar
, as we expected.
如我们所料,这将打印出条形图
。
So just remind yourself: always pass a function into then()
!
因此,只要提醒自己:一定要将函数传入then()!
Solving the puzzle 解决难题
Now that we've learned everything there is to know about promises (or close to it!), we should be able to solve the puzzle I originally posed at the start of this post.
既然我们已经了解了关于承诺的所有知识(或接近于知识!),我们就应该能够解决我在本篇文章开头提出的难题了。
Here is the answer to each one, in graphical format so you can better visualize it:
下面是每个问题的答案,以图表的形式呈现,让您更直观地了解答案:
Puzzle #1 谜题 #1
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);
Answer: 请回答:
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|
Puzzle #2 谜题 #2
doSomething().then(function () {
doSomethingElse();
}).then(finalHandler);
Answer: 请回答:
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(undefined)
|------------------|
Puzzle #3 谜题 #3
doSomething().then(doSomethingElse())
.then(finalHandler);
Answer: 请回答:
doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
finalHandler(resultOfDoSomething)
|------------------|
Puzzle #4 谜题 #4
doSomething().then(doSomethingElse)
.then(finalHandler);
Answer: 请回答:
doSomething
|-----------------|
doSomethingElse(resultOfDoSomething)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|
If these answers still don't make sense, then I encourage you to re-read the post, or to define the doSomething()
and doSomethingElse()
methods and try it out yourself in your browser.
如果这些答案仍然没有意义,那么我建议您重新阅读这篇文章,或者定义doSomething()
和doSomethingElse()
方法,并在浏览器中亲自尝试一下。
Clarification: for these examples, I’m assuming that both doSomething()
and doSomethingElse()
return promises, and that those promises represent something done outside of the JavaScript event loop (e.g. IndexedDB, network, setTimeout
), which is why they’re shown as being concurrent when appropriate. Here’s a JSBin to demonstrate.
澄清:在这些示例中,我假定doSomething()
和doSomethingElse()
都会返回承诺,并且这些承诺代表在 JavaScript 事件循环之外完成的事情(例如 IndexedDB、network、setTimeout
),这就是为什么在适当的时候它们被显示为并发的原因。下面是一个JSBin演示。
And for more advanced uses of promises, check out my promise protips cheat sheet.
如需了解承诺的更多高级用法,请查看我的承诺技巧小抄。
Final word about promises
关于承诺的最后一句话
Promises are great. If you are still using callbacks, I strongly encourage you to switch over to promises. Your code will become smaller, more elegant, and easier to reason about.
许诺非常好。如果您仍在使用回调,我强烈建议您改用承诺。您的代码将变得更小、更优雅、更易于推理。
And if you don't believe me, here's proof: a refactor of PouchDB's map/reduce module to replace callbacks with promises. The result: 290 insertions, 555 deletions.
如果你不相信我,这里有证据:重构 PouchDB 的 map/reduce 模块,用承诺取代回调。结果是:插入 290 次,删除 555 次。
Incidentally, the one who wrote that nasty callback code was… me! So this served as my first lesson in the raw power of promises, and I thank the other PouchDB contributors for coaching me along the way.
顺便说一句,写下那段恶心的回调代码的人是......我!因此,这是我第一次领教承诺的威力,感谢 PouchDB 的其他贡献者一路上对我的指导。
That being said, promises aren't perfect. It's true that they're better than callbacks, but that's a lot like saying that a punch in the gut is better than a kick in the teeth. Sure, one is preferable to the other, but if you had a choice, you'd probably avoid them both.
话虽如此,承诺并不完美。诚然,承诺比回电好,但这就好比说一拳打在肚子上比一脚踢在牙齿上好。当然,一个比另一个好,但如果你有选择,你可能会同时避开它们。
While superior to callbacks, promises are still difficult to understand and error-prone, as evidenced by the fact that I felt compelled to write this blog post. Novices and experts alike will frequently mess this stuff up, and really, it's not their fault. The problem is that promises, while similar to the patterns we use in synchronous code, are a decent substitute but not quite the same.
虽然承诺比回调更优越,但它仍然难以理解,而且容易出错,我不得不写这篇博文就是明证。无论是新手还是专家,都会经常把这些东西弄得一团糟,其实这并不是他们的错。问题在于,承诺虽然与我们在同步代码中使用的模式相似,但只是一个不错的替代品,并不完全相同。
In truth, you shouldn't have to learn a bunch of arcane rules and new APIs to do things that, in the synchronous world, you can do perfectly well with familiar patterns like return
, catch
, throw
, and for-loops. There shouldn't be two parallel systems that you have to keep straight in your head at all times.
事实上,在同步世界中,你完全可以用熟悉的模式(如return
、catch
、throw
和 for-loop)来完成一些事情,而不需要学习一大堆神秘的规则和新的 API。不应该有两个并行的系统,让你必须时刻保持头脑清醒。
Awaiting async/await 等待 async/await
That's the point I made in "Taming the asynchronous beast with ES7", where I explored the ES7 async
/await
keywords, and how they integrate promises more deeply into the language. Instead of having to write pseudo-synchronous code (with a fake catch()
method that's kinda like catch
, but not really), ES7 will allow us to use the real try
/catch
/return
keywords, just like we learned in CS 101.
这就是我在"用 ES7 驯服异步野兽 "一文中提出的观点,我在文中探讨了 ES7 的async/await
关键字,以及它们如何将承诺更深入地集成到语言中。ES7 将允许我们使用真正的try/catch/return
关键字,就像我们在 CS 101 中学习的那样,而不是编写伪同步代码(使用有点像catch
但又不是真的catch
的假catch
()
方法)。
This is a huge boon to JavaScript as a language. Because in the end, these promise anti-patterns will still keep cropping up, as long as our tools don't tell us when we're making a mistake.
这对 JavaScript 这门语言来说是一大福音。因为归根结底,只要我们的工具不在我们犯错时告诉我们,这些承诺反模式仍会不断出现。
To take an example from JavaScript's history, I think it's fair to say that JSLint and JSHint did a greater service to the community than JavaScript: The Good Parts, even though they effectively contain the same information. It's the difference between being told exactly the mistake you just made in your code, as opposed to reading a book where you try to understand other people's mistakes.
举一个 JavaScript 历史上的例子,我认为可以说JSLint和JSHint比JavaScript 为社区做出了更大的贡献:The Good Parts》对社区的贡献更大,尽管它们实际上包含了相同的信息。这就好比你在代码中犯了什么错误,别人会准确地告诉你,而你却在阅读一本书,试图理解别人的错误。
The beauty of ES7 async
/await
is that, for the most part, your mistakes will reveal themselves as syntax/compiler errors rather than subtle runtime bugs. Until then, though, it's good to have a grasp of what promises are capable of, and how to use them properly in ES5 and ES6.
ES7async/await
的妙处在于,在大多数情况下,您的错误会以语法/编译器错误的形式暴露出来,而不是以微妙的运行时 bug 的形式暴露出来。不过在此之前,最好还是先了解一下承诺的功能,以及如何在 ES5 和 ES6 中正确使用它们。
So while I recognize that, like JavaScript: The Good Parts, this blog post can only have a limited impact, it's hopefully something you can point people to when you see them making these same mistakes. Because there are still way too many of us who just need to admit: "I have a problem with promises!"
《因此,虽然我承认,与JavaScript:The Good Parts》一样,这篇博文只能产生有限的影响,但我希望当你看到别人犯同样的错误时,你可以指给他们看。因为我们中仍有太多的人需要承认:"我对承诺有意见!"
Update: it’s been pointed out to me that Bluebird 3.0 will print out warnings that can prevent many of the mistakes I’ve identified in this post. So using Bluebird is another great option while we wait for ES7!
更新:有人向我指出,Bluebird 3.0 会打印出警告,可以避免我在这篇文章中指出的许多错误。因此,在等待 ES7 的过程中,使用 Bluebird 是另一个不错的选择!