Promises simplify deferred and asynchronous computations. A promise represents an operation that hasn't completed yet.
承诺简化了延迟和异步计算。承诺表示尚未完成的操作。
Developers, prepare yourself for a pivotal moment in the history of
web development.
开发人员,准备好迎接网络开发史上的关键时刻吧。
[Drumroll begins] [鼓声响起]
Promises have arrived in JavaScript!
JavaScript 中的承诺已经到来!
[Fireworks explode, glittery paper rains from above, the crowd goes wild]
[烟花爆炸,闪闪发光的纸片从天而降,人群沸腾]
At this point you fall into one of these categories:
在这一点上,你属于其中一类:
- People are cheering around you, but you're not sure what all the fuss is
about. Maybe you're not even sure what a "promise" is. You'd shrug, but the
weight of glittery paper is weighing down on your shoulders. If so, don't
worry about it, it took me ages to work out why I should care about this
stuff. You probably want to begin at the beginning.
周围的人都在为你欢呼,但你却不知道这一切究竟是为什么。也许你连 "承诺 "是什么都不清楚。你本想耸耸肩,但闪闪发光的纸张压得你喘不过气来。如果是这样,不用担心,我也是花了好久才想明白为什么要关心这些东西。你可能想从头开始。 - You punch the air! About time right? You've used these Promise things before
but it bothers you that all implementations have a slightly different API.
What's the API for the official JavaScript version? You probably want to begin
with the terminology.
你打了空气一拳!是时候了,对吧?你以前用过 Promise,但所有实现方式的 API 都略有不同,这让你很烦恼。官方 JavaScript 版本的 API 是什么?你可能想从术语入手。 - You knew about this already and you scoff at those who are jumping up and
down like it's news to them. Take a moment to bask in your own superiority,
then head straight to the API reference.
你早就知道了,却对那些上蹿下跳的人嗤之以鼻,好像这对他们来说是个新闻。花点时间沉浸在自己的优越感中,然后直奔API 参考资料。
Browser support and polyfill
浏览器支持和多填充
To bring browsers that lack a complete promises implementation up to spec
compliance, or add promises to other browsers and Node.js, check out
the polyfill
(2k gzipped).
要使缺乏完整承诺实现的浏览器符合规范,或将承诺添加到其他浏览器和 Node.js,请查看polyfill(2k gzipped)。
What's all the fuss about?
这有什么好大惊小怪的?
JavaScript is single threaded, meaning that two bits of script cannot run at
the same time; they have to run one after another. In browsers, JavaScript
shares a thread with a load of other stuff that differs from browser to
browser. But typically JavaScript is in the same queue as painting, updating
styles, and handling user actions (such as highlighting text and interacting
with form controls). Activity in one of these things delays the others.
JavaScript 是单线程的,这意味着两个脚本不能同时运行;它们必须一个接一个地运行。在浏览器中,JavaScript 与其他程序共享一个线程,不同浏览器共享的线程也不同。但通常情况下,JavaScript 与绘画、更新样式和处理用户操作(如突出显示文本和与表单控件交互)处于同一队列。其中一项活动会延迟其他活动。
As a human being, you're multithreaded. You can type with multiple fingers,
you can drive and hold a conversation at the same time. The only blocking
function we have to deal with is sneezing, where all current activity must
be suspended for the duration of the sneeze. That's pretty annoying,
especially when you're driving and trying to hold a conversation. You don't
want to write code that's sneezy.
作为一个人,你是多线程的。你可以用多个手指打字,可以一边开车一边聊天。我们唯一需要处理的阻塞功能就是打喷嚏,在打喷嚏期间,所有当前活动都必须暂停。这非常烦人,尤其是当你一边开车一边试图进行对话时。你可不想写出打喷嚏的代码。
You've probably used events and callbacks to get around this. Here are events:
您可能使用过事件和回调来解决这个问题。下面是事件:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
This isn't sneezy at all. We get the image, add a couple of listeners, then
JavaScript can stop executing until one of those listeners is called.
这一点也不难。我们获取图像,添加几个监听器,然后 JavaScript 就可以停止执行,直到其中一个监听器被调用。
Unfortunately, in the example above, it's possible that the events happened
before we started listening for them, so we need to work around that using
the "complete" property of images:
不幸的是,在上面的示例中,事件有可能在我们开始监听之前就发生了,因此我们需要使用图像的 "complete "属性来解决这个问题:
var img1 = document.querySelector('.img-1');
function loaded() {
// woo yey image loaded
}
if (img1.complete) {
loaded();
}
else {
img1.addEventListener('load', loaded);
}
img1.addEventListener('error', function() {
// argh everything's broken
});
This doesn't catch images that errored before we got a chance to listen for
them; unfortunately the DOM doesn't give us a way to do that. Also, this is
loading one image. Things get even more complex if we want to know when a set
of images have loaded.
这并不能捕捉到在我们有机会监听之前出错的图片;不幸的是,DOM 并没有给我们提供这样做的方法。此外,这只是加载一张图片。如果我们想知道一组图片何时加载完毕,情况就会变得更加复杂。
Events aren't always the best way
Events are great for things that can happen multiple times on the same
object—keyup
, touchstart
etc. With those events you don't really care
about what happened before you attached the listener. But when it comes to
async success/failure, ideally you want something like this:
事件非常适合在同一对象上发生多次的情况--按键
、触摸启动
等。对于这些事件,你并不关心在附加监听器之前发生了什么。但当涉及到异步成功/失败时,理想情况下您需要这样的事件:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
This is what promises do, but with better naming. If HTML image elements had a
"ready" method that returned a promise, we could do this:
这就是承诺的作用,但命名更好。如果 HTML 图像元素有一个返回承诺的 "ready "方法,我们就能做到这一点:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
At their most basic, promises are a bit like event listeners except:
从最基本的角度看,承诺有点像事件监听器,只不过:
- A promise can only succeed or fail once. It cannot succeed or fail twice,
neither can it switch from success to failure or vice versa.
承诺只能成功或失败一次。它不能成功或失败两次,也不能从成功切换到失败,反之亦然。 - If a promise has succeeded or failed and you later add a success/failure
callback, the correct callback will be called, even though the event took
place earlier.
如果承诺已成功或失败,而您随后又添加了成功/失败回调,则会调用正确的回调,即使事件发生得更早。
This is extremely useful for async success/failure, because you're less
interested in the exact time something became available, and more interested
in reacting to the outcome.
这对异步成功/失败非常有用,因为你对某些东西可用的确切时间不那么感兴趣,而对结果的反应更感兴趣。
Promise terminology
Domenic Denicola proof read the first draft
of this article and graded me "F" for terminology. He put me in detention,
forced me to copy out
States and Fates
100 times, and wrote a worried letter to my parents. Despite that, I still
get a lot of the terminology mixed up, but here are the basics:
多米尼克-德尼科拉(Domenic Denicola)校对了这篇文章的初稿,并给我打了 "F "的术语分数。他把我关进了拘留所,强迫我把 "国家 "和 "命运 "抄写 100 遍,还给我父母写了一封忧心忡忡的信。尽管如此,我还是把很多术语搞混了,但以下是基本术语:
A promise can be: 承诺可以是
- fulfilled - The action relating to the promise succeeded
fulfilled- 与承诺有关的行动取得成功 - rejected - The action relating to the promise failed
rejected- 与承诺有关的行动失败 - pending - Hasn't fulfilled or rejected yet
pending- 尚未实现或拒绝 - settled - Has fulfilled or rejected
定居- 已实现或拒绝
The spec
also uses the term thenable to describe an object that is promise-like,
in that it has a then
method. This term reminds me of ex-England Football
Manager Terry Venables so
I'll be using it as little as possible.
规范还使用了thenable
这个术语来描述一个类似于 promise的
对象,因为它有一个then
方法。这个术语让我想起了前英格兰足球经理特里-维纳布尔斯(Terry Venables),所以我会尽量少用它。
Promises arrive in JavaScript!
Promises have been around for a while in the form of libraries, such as:
许诺以图书馆的形式出现已有一段时间,如
The above and JavaScript promises share a common, standardized behaviour
called Promises/A+. If
you're a jQuery user, they have something similar called
Deferreds. However,
Deferreds aren't Promise/A+ compliant, which makes them
subtly different and less useful,
so beware. jQuery also has
a Promise type, but this is just a
subset of Deferred and has the same issues.
上述承诺和 JavaScript 承诺共享一种通用的标准化行为,称为Promises/A+。如果你是 jQuery 用户,他们也有类似的功能,叫做Deferreds。然而,Deferreds 并不兼容 Promise/A+,这使得它们之间存在微妙的差异,而且实用性较低,所以要当心。jQuery 也有Promise 类型,但它只是 Deferred 的子集,也存在同样的问题。
Although promise implementations follow a standardized behaviour, their
overall APIs differ. JavaScript promises are similar in API to RSVP.js.
Here's how you create a promise:
尽管 promise 实现遵循标准化的行为,但它们的整体 API 却各不相同。JavaScript 承诺的 API 与 RSVP.js 类似。下面是创建 promise 的方法:
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
The promise constructor takes one argument, a callback with two parameters,
resolve and reject. Do something within the callback, perhaps async, then call
resolve if everything worked, otherwise call reject.
promise 构造函数只有一个参数,即带有两个参数(resolve 和 reject)的回调。在回调中做一些事情,也许是同步的,然后在一切正常的情况下调用解析,否则调用拒绝。
Like throw
in plain old JavaScript, it's customary, but not required, to
reject with an Error object. The benefit of Error objects is they capture a
stack trace, making debugging tools more helpful.
就像在
普通的 JavaScript 中一样,使用 Error 对象进行拒绝是一种习惯做法,但并非必需。Error 对象的好处是可以捕获堆栈跟踪,从而使调试工具更有帮助。
Here's how you use that promise:
下面是如何使用这一承诺的方法:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then()
takes two arguments, a callback for a success case, and another
for the failure case. Both are optional, so you can add a callback for the
success or failure case only.then()
有两个参数,一个是成功情况下
的回调,另一个是失败情况下的回调。这两个参数都是可选的,因此您可以只为成功或失败情况添加回调。
JavaScript promises started out in the DOM as "Futures", renamed to "Promises",
and finally moved into JavaScript. Having them in JavaScript rather than the
DOM is great because they'll be available in non-browser JS contexts such as
Node.js (whether they make use of them in their core APIs is another question).
JavaScript promises 最初在 DOM 中称为 "Futures",后来更名为 "Promises",最后移到了 JavaScript 中。将它们放在 JavaScript 而不是 DOM 中是件好事,因为它们可以在 Node.js 等非浏览器 JS 环境中使用(至于它们是否会在其核心 API 中使用它们,则是另一个问题)。
Although they're a JavaScript feature, the DOM isn't afraid to use them. In
fact, all new DOM APIs with async success/failure methods will use promises.
This is happening already with
Quota Management,
Font Load Events,
ServiceWorker,
Web MIDI,
Streams, and more.
虽然承诺是 JavaScript 的一项功能,但 DOM 并不害怕使用它们。事实上,所有具有异步成功/失败方法的新 DOM API 都将使用承诺。配额管理(Quota Management)、字体加载事件(Font Load Events)、服务工作器(ServiceWorker)、网络 MIDI、数据流等都已使用了承诺。
Compatibility with other libraries
The JavaScript promises API will treat anything with a then()
method as
promise-like (or thenable
in promise-speak sigh), so if you use a library
that returns a Q promise, that's fine, it'll play nice with the new
JavaScript promises.
JavaScript 承诺 API 会将任何带有then()
方法的内容视为类似于承诺的内容(或者用承诺的术语来说是 thenable
),因此如果您使用的库返回的是 Q 承诺,那也没关系,它可以与新的 JavaScript 承诺很好地兼容。
Although, as I mentioned, jQuery's Deferreds are a bit … unhelpful.
Thankfully you can cast them to standard promises, which is worth doing
as soon as possible:
虽然,正如我提到的,jQuery的Deferreds是有点......无助。值得庆幸的是,你可以将它们转换成标准的承诺,这值得尽快去做:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
Here, jQuery's $.ajax
returns a Deferred. Since it has a then()
method,
Promise.resolve()
can turn it into a JavaScript promise. However,
sometimes deferreds pass multiple arguments to their callbacks, for example:
在这里,jQuery 的$.ajax
返回一个延迟。由于它有一个then()
方法,Promise.resolve
(
)
可以将其转化为 JavaScript 承诺。不过,有时延迟会向其回调传递多个参数,例如
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
Whereas JS promises ignore all but the first:
而联署材料的承诺除了第一条外,其他都忽略不计:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
Thankfully this is usually what you want, or at least gives you access to
what you want. Also, be aware that jQuery doesn't follow the convention of
passing Error objects into rejections.
值得庆幸的是,这通常是你想要的,或者至少能让你获得你想要的。此外,请注意 jQuery 并不遵循将 Error 对象传入拒绝的惯例。
Complex async code made easier
复杂的异步代码变得更简单
Right, let's code some things. Say we want to:
好了,让我们来编写一些代码。假设我们想
- Start a spinner to indicate loading
启动旋转器,显示装载情况 - Fetch some JSON for a story, which gives us the title, and urls for each chapter
获取故事的 JSON 文件,其中包含标题和各章节的网址 - Add title to the page
为页面添加标题 - Fetch each chapter 获取每个章节
- Add the story to the page
将故事添加到页面 - Stop the spinner 停止旋转器
… but also tell the user if something went wrong along the way. We'll want
to stop the spinner at that point too, else it'll keep on spinning, get
dizzy, and crash into some other UI.
......但如果途中出了问题,也要告诉用户。我们也希望在这个时候停止旋转器,否则它会继续旋转,让人头晕目眩,然后撞到其他用户界面上。
Of course, you wouldn't use JavaScript to deliver a story,
serving as HTML is faster,
but this pattern is pretty common when dealing with APIs: Multiple data
fetches, then do something when it's all done.
当然,您不会使用 JavaScript 来提供故事,因为 HTML 更快,但这种模式在处理应用程序接口时非常常见:多次获取数据,然后在全部完成后做一些事情。
To start with, let's deal with fetching data from the network:
首先,让我们来处理从网络获取数据的问题:
Promisifying XMLHttpRequest
承诺 XMLHttpRequest
Old APIs will be updated to use promises, if it's possible in a backwards
compatible way. XMLHttpRequest
is a prime candidate, but in the mean time
let's write a simple function to make a GET request:
如果可以向后兼容,旧的应用程序接口将更新为使用承诺。XMLHttpRequest
就是一个主要的候选者,但与此同时,让我们编写一个简单的函数来发出 GET 请求:
function get(url) {
// Return a new promise.
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
resolve(req.response);
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.send();
});
}
Now let's use it: 现在让我们来使用它:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
Now we can make HTTP requests without manually typing XMLHttpRequest
, which is great, because the
less I have to see the infuriating camel-casing of XMLHttpRequest
, the happier my life will be.
现在,我们无需手动键入XMLHttpRequest
就能发出 HTTP 请求,这真是太好了,因为我越少看到XMLHttpRequest
这种令人恼火的驼峰包,我的生活就越快乐。
Chaining
then()
isn't the end of the story, you can chain then
s together to
transform values or run additional async actions one after another.the
n
()
并不是故事的终结,您可以将then()
链接起来以转换值,或一个接一个地运行其他异步操作。
Transforming values
You can transform values simply by returning the new value:
只需返回新值,就可以转换数值:
var promise = new Promise(function(resolve, reject) {
resolve(1);
});
promise.then(function(val) {
console.log(val); // 1
return val + 2;
}).then(function(val) {
console.log(val); // 3
})
As a practical example, let's go back to:
举个实际的例子,让我们回到:
get('story.json').then(function(response) {
console.log("Success!", response);
})
The response is JSON, but we're currently receiving it as plain text. We
could alter our get function to use the JSON
responseType
,
but we could also solve it in promises land:
响应是 JSON 格式的,但我们目前接收的是纯文本。我们可以修改 get 函数以使用 JSONresponseType
,但我们也可以通过承诺来解决这个问题:
get('story.json').then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log("Yey JSON!", response);
})
Since JSON.parse()
takes a single argument and returns a transformed value,
we can make a shortcut:
由于JSON.parse()
只需要一个参数,并返回一个转换后的值,因此我们可以使用捷径:
get('story.json').then(JSON.parse).then(function(response) {
console.log("Yey JSON!", response);
})
In fact, we could make a getJSON()
function really easily:
事实上,我们可以很容易地创建一个getJSON()
函数:
function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON()
still returns a promise, one that fetches a url then parses
the response as JSON.getJSON()
返回的仍然是一个承诺,一个获取 url 然后将响应解析为 JSON 的承诺。
Queuing asynchronous actions
You can also chain then
s to run async actions in sequence.
您还可以将thens
连锁起来,依次运行异步操作。
When you return something from a then()
callback, it's a bit magic.
If you return a value, the next then()
is called with that value. However,
if you return something promise-like, the next then()
waits on it, and is
only called when that promise settles (succeeds/fails). For example:
当你从then()
回调中返回一些东西时,这就有点神奇了。如果返回一个值,下一个then()
就会调用该值。但是,如果您返回的是
类似承诺的内容,下一个then()
就会等待它,只有当该承诺确定(成功/失败)时才会调用。例如
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
Here we make an async request to story.json
, which gives us a set of
URLs to request, then we request the first of those. This is when promises
really start to stand out from simple callback patterns.
在这里,我们向story.json
发起一个异步请求,它会给出一组要请求的 URL,然后我们请求其中的第一个。此时,承诺才真正开始从简单的回调模式中脱颖而出。
You could even make a shortcut method to get chapters:
你甚至可以制作一个获取章节的快捷方法:
var storyPromise;
function getChapter(i) {
storyPromise = storyPromise || getJSON('story.json');
return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})
}
// and using it is simple:
getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);
}).then(function(chapter) {
console.log(chapter);
})
We don't download story.json
until getChapter
is called, but the next
time(s) getChapter
is called we reuse the story promise, so story.json
is only fetched once. Yay Promises!
在调用getChapter
之前,我们不会下载story.json
,但在下一次调用getChapter
时,我们会重复使用故事承诺,因此story.json
只被获取一次。承诺万岁
Error handling
As we saw earlier, then()
takes two arguments, one for success, one
for failure (or fulfill and reject, in promises-speak):
正如我们前面所看到的,then()
需要两个参数,一个表示成功,一个表示失败(用承诺的话来说,就是履行和拒绝):
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
You can also use catch()
:
您也可以使用catch():
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
There's nothing special about catch()
, it's just sugar for
then(undefined, func)
, but it's more readable. Note that the two code
examples above do not behave the same, the latter is equivalent to:catch()
并没有什么特别之处,它只是then(undefined, func)
的 "糖"
,
但它更易读。请注意,上述两个代码示例的行为并不相同,后者等同于
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
The difference is subtle, but extremely useful. Promise rejections skip
forward to the next then()
with a rejection callback (or catch()
, since
it's equivalent). With then(func1, func2)
, func1
or func2
will be
called, never both. But with then(func1).catch(func2)
, both will be
called if func1
rejects, as they're separate steps in the chain. Take
the following:
两者的区别很微妙,但却非常有用。拒绝允诺会跳转到下一个带有拒绝回调的then()
(或catch()
,因为它们是等价的)。使用then(func1,func2)
,只会调用func1
或func2
,而不会同时调用两个。但使用then(func1).catch(func2)
时,如果func1
被拒绝,则
会同时调用这两个函数,因为它们是链中的独立步骤。请看下面的例子:
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don't worry about it");
}).then(function() {
console.log("All done!");
})
The flow above is very similar to normal JavaScript try/catch, errors that
happen within a "try" go immediately to the catch()
block. Here's the
above as a flowchart (because I love flowcharts):
上述流程与普通的 JavaScript try/catch 非常相似,在 "try "中发生的错误会立即进入catch()
块。下面是上述流程图(因为我喜欢流程图):
Follow the blue lines for promises that fulfill, or the red for ones that
reject.
蓝线代表兑现的承诺,红线代表拒绝的承诺。
JavaScript exceptions and promises
JavaScript 异常和承诺
Rejections happen when a promise is explicitly rejected, but also implicitly
if an error is thrown in the constructor callback:
拒绝会在承诺被明确拒绝时发生,但如果在构造函数回调中抛出错误,拒绝也会隐式发生:
var jsonPromise = new Promise(function(resolve, reject) {
// JSON.parse throws an error if you feed it some
// invalid JSON, so this implicitly rejects:
resolve(JSON.parse("This ain't JSON"));
});
jsonPromise.then(function(data) {
// This never happens:
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
This means it's useful to do all your promise-related work inside the
promise constructor callback, so errors are automatically caught and
become rejections.
这意味着在 promise 构造函数的回调中完成所有与 promise 相关的工作是非常有用的,这样错误就会被自动捕获并变成拒绝。
The same goes for errors thrown in then()
callbacks.
在then()
回调中抛出的错误也是如此。
get('/').then(JSON.parse).then(function() {
// This never happens, '/' is an HTML page, not JSON
// so JSON.parse throws
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
Error handling in practice
错误处理实践
错误处理实践
With our story and chapters, we can use catch to display an error to the user:
通过我们的故事和章节,我们可以使用 catch 向用户显示错误:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
addHtmlToPage(chapter1.html);
}).catch(function() {
addTextToPage("Failed to show chapter");
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
If fetching story.chapterUrls[0]
fails (e.g., http 500 or user is offline),
it'll skip all following success callbacks, which includes the one in
getJSON()
which tries to parse the response as JSON, and also skips the
callback that adds chapter1.html to the page. Instead it moves onto the catch
callback. As a result, "Failed to show chapter" will be added to the page if
any of the previous actions failed.
如果获取story.chapterUrls[0]
失败(例如 http 500 或用户离线),它将跳过下面所有的成功回调,包括getJSON()
中试图将响应解析为 JSON 的回调,也会跳过将 chapter1.html 添加到页面的回调。取而代之的是 catch 回调。因此,如果之前的任何操作失败,页面中将添加 "Failed to show chapter"(显示章节失败)。
Like JavaScript's try/catch, the error is caught and subsequent code
continues, so the spinner is always hidden, which is what we want. The
above becomes a non-blocking async version of:
就像 JavaScript 的 try/catch,错误被捕获后,后续代码会继续执行,因此旋转器始终是隐藏的,这正是我们想要的。上述代码就变成了非阻塞异步版本:
try {
var story = getJSONSync('story.json');
var chapter1 = getJSONSync(story.chapterUrls[0]);
addHtmlToPage(chapter1.html);
}
catch (e) {
addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'
You may want to catch()
simply for logging purposes, without recovering
from the error. To do this, just rethrow the error. We could do this in
our getJSON()
method:
您可能只想使用catch()
来记录日志,而不想从错误中恢复。为此,只需重新抛出错误即可。我们可以在getJSON()
方法中这样做:
function getJSON(url) {
return get(url).then(JSON.parse).catch(function(err) {
console.log("getJSON failed for", url, err);
throw err;
});
}
So we've managed to fetch one chapter, but we want them all. Let's make
that happen.
我们已经找到了一个章节,但我们想要全部章节。让我们实现这个愿望吧。
Parallelism and sequencing: getting the best of both
Thinking async isn't easy. If you're struggling to get off the mark,
try writing the code as if it were synchronous. In this case:
异步思维并不容易。如果你很难脱口而出,试着把代码写成同步代码。在这种情况下
try {
var story = getJSONSync('story.json');
addHtmlToPage(story.heading);
story.chapterUrls.forEach(function(chapterUrl) {
var chapter = getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}
catch (err) {
addTextToPage("Argh, broken: " + err.message);
}
document.querySelector('.spinner').style.display = 'none'
That works! But it's sync and locks up the browser while things download. To
make this work async we use then()
to make things happen one after another.
这很有效!但它是同步的,下载时会锁定浏览器。为了实现同步,我们可以使用then()
来让事情一个接一个地发生。
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
But how can we loop through the chapter urls and fetch them in order? This
doesn't work:
但我们如何才能循环浏览章节 URL 并按顺序获取它们呢?这行不通:
story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
})
forEach
isn't async-aware, so our chapters would appear in whatever order
they download, which is basically how Pulp Fiction was written. This isn't
Pulp Fiction, so let's fix it.forEach
没有异步感知功能,因此我们的章节将按照下载的顺序显示,这基本上就是《纸浆小说》的编写过程。这不是《纸浆小说》,所以让我们来解决它。
Creating a sequence
We want to turn our chapterUrls
array into a sequence of promises. We can do that using then()
:
我们要将chapterUrls
数组转化为一系列承诺。我们可以使用then()
来实现:
// Start off with a promise that always resolves
var sequence = Promise.resolve();
// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
// Add these actions to the end of the sequence
sequence = sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
})
This is the first time we've seen Promise.resolve()
, which creates a
promise that resolves to whatever value you give it. If you pass it an
instance of Promise
it'll simply return it (note: this is a
change to the spec that some implementations don't yet follow). If you
pass it something promise-like (has a then()
method), it creates a
genuine Promise
that fulfills/rejects in the same way. If you pass
in any other value, e.g., Promise.resolve('Hello')
, it creates a
promise that fulfills with that value. If you call it with no value,
as above, it fulfills with "undefined".
这是我们第一次看到Promise.resolve()
,它会创建一个承诺,并解析为您给它的任何值。如果你传递给它一个Promise
实例,它就会简单地返回它(注意:这是对规范的修改,有些实现还没有遵循)。如果你传给它一个类似Promise
的东西(有一个then()
方法),它就会创建一个真正的Promise
,以同样的方式实现/拒绝。如果你传入任何其他值,例如Promise.resolve('Hello')
,它就会创建一个履行该值的承诺。如果调用时没有输入任何值(如上所示),则会以 "undefined"(未定义)来履行。
There's also Promise.reject(val)
, which creates a promise that rejects with
the value you give it (or undefined).
还有Promise.rejection(val)
,它创建的承诺会根据你给它的值(或未定义的值)进行拒绝。
We can tidy up the above code using
array.reduce
:
我们可以使用array.reduce
整理上述代码:
// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Add these actions to the end of the sequence
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve())
This is doing the same as the previous example, but doesn't need the separate
"sequence" variable. Our reduce callback is called for each item in the array.
"sequence" is Promise.resolve()
the first time around, but for the rest of the
calls "sequence" is whatever we returned from the previous call. array.reduce
is really useful for boiling an array down to a single value, which in this case
is a promise.
这与上一个示例相同,但不需要单独的 "sequence "变量。数组中的每个项目都会调用我们的 reduce 回调。第一次调用时,"sequence
"是Promise.resolve()
,但在其余的调用中,"sequence "是我们从上一次调用中返回的值。array.reduce
在将数组精简为单个值时非常有用,在本例中就是一个 promise。
Let's put it all together:
让我们一起来看看:
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Once the last chapter's promise is done…
return sequence.then(function() {
// …fetch the next chapter
return getJSON(chapterUrl);
}).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
And there we have it, a fully async version of the sync version. But we can do
better. At the moment our page is downloading like this:
这就是同步版本的完全异步版本。但我们可以做得更好。目前,我们的页面是这样下载的:
Browsers are pretty good at downloading multiple things at once, so we're losing
performance by downloading chapters one after the other. What we want to do is
download them all at the same time, then process them when they've all arrived.
Thankfully there's an API for this:
浏览器很擅长同时下载多个文件,因此我们一个接一个地下载章节会降低性能。我们要做的是同时下载所有章节,然后在它们全部到达时进行处理。幸好有一个 API 可以实现这一点:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all
takes an array of promises and creates a promise that fulfills
when all of them successfully complete. You get an array of results (whatever
the promises fulfilled to) in the same order as the promises you passed in.Promise.all
接收一个承诺数组,并创建一个承诺,当所有承诺都成功完成时,该承诺就会实现。您会得到一个结果数组(无论承诺实现了什么),其顺序与您传入的承诺相同。
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters) {
// Now we have the chapters jsons in order! Loop through…
chapters.forEach(function(chapter) {
// …and add to the page
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened so far
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
Depending on connection, this can be seconds faster than loading one-by-one,
and it's less code than our first try. The chapters can download in whatever
order, but they appear on screen in the right order.
视连接情况而定,这可能比逐个下载快几秒钟,而且比我们第一次尝试的代码少。章节的下载顺序不限,但它们会按正确的顺序出现在屏幕上。
However, we can still improve perceived performance. When chapter one arrives we
should add it to the page. This lets the user start reading before the rest of
the chapters have arrived. When chapter three arrives, we wouldn't add it to the
page because the user may not realize chapter two is missing. When chapter two
arrives, we can add chapters two and three, etc etc.
不过,我们仍然可以提高感知性能。当第一章到来时,我们应该将其添加到页面中。这样,用户就可以在其他章节到达之前开始阅读。当第三章到达时,我们不会把它添加到页面上,因为用户可能不会意识到缺少了第二章。当第二章到达时,我们可以添加第二章和第三章,等等等等。
To do this, we fetch JSON for all our chapters at the same time, then create a
sequence to add them to the document:
为此,我们要同时获取所有章节的 JSON 文件,然后创建一个序列将它们添加到文档中:
getJSON('story.json')
.then(function(story) {
addHtmlToPage(story.heading);
// Map our array of chapter urls to
// an array of chapter json promises.
// This makes sure they all download in parallel.
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// Use reduce to chain the promises together,
// adding content to the page for each chapter
return sequence
.then(function() {
// Wait for everything in the sequence so far,
// then wait for this chapter to arrive.
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
And there we go, the best of both! It takes the same amount of time to deliver
all the content, but the user gets the first bit of content sooner.
就这样,两者兼得!提供所有内容所需的时间是一样的,但用户可以更快地看到第一部分内容。
In this trivial example, all of the chapters arrive around the same time, but
the benefit of displaying one at a time will be exaggerated with more, larger
chapters.
在这个微不足道的例子中,所有章节都是同时到达的,但如果章节越多、越大,一次显示一个章节的好处就越明显。
Doing the above with Node.js-style callbacks or
events is around
double the code, but more importantly isn't as easy to follow. However, this
isn't the end of the story for promises, when combined with other ES6 features
they get even easier.
如果使用Node.js 风格的回调或事件来实现上述功能,代码量要多出一倍左右,更重要的是不容易理解。不过,这并不是承诺的终结,当与 ES6 的其他功能相结合时,承诺会变得更加简单。
Bonus round: expanded capabilities
Since I originally wrote this article, the ability to use Promises has expanded
greatly. Since Chrome 55, async functions have allowed promise-based code to be
written as if it were synchronous, but without blocking the main thread. You can
read more about that in my async functions article. There's
widespread support for both Promises and async functions in the major browsers.
You can find the details in MDN's
Promise
and async
function
reference.
自从我最初撰写这篇文章以来,使用许诺的功能已大大扩展。从 Chrome 55 开始,异步函数允许基于承诺的代码像同步代码一样编写,但不会阻塞主线程。你可以在我的异步函数文章中了解更多相关信息。在主流浏览器中,Promises 和 async 函数都得到了广泛支持。你可以在 MDN 的Promise和async 函数参考中找到详细信息。
Many thanks to Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp,
Addy Osmani, Arthur Evans, and Yutaka Hirano who proofread this and made
corrections/recommendations.
非常感谢 Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、Addy Osmani、Arthur Evans 和 Yutaka Hirano 对本文进行校对并提出修改/建议。
Also, thanks to Mathias Bynens for
updating various parts
of the article.
另外,感谢Mathias Bynens 更新了文章的各个部分。