1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
|
---
title: 使用 Promise
slug: Web/JavaScript/Guide/Using_promises
tags:
- Guide
- JavaScript
- Promise
- 中级
- 异步
- 指南
translation_of: Web/JavaScript/Guide/Using_promises
---
<div>{{jsSidebar("JavaScript Guide")}}{{PreviousNext("Web/JavaScript/Guide/Details_of_the_Object_Model", "Web/JavaScript/Guide/Iterators_and_Generators")}}</div>
<p>{{jsxref("Promise")}} 是一个对象,它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的 Promise 实例对象,所以本教程将首先说明怎样使用 Promise,再说明如何创建 Promise。</p>
<p>本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。</p>
<p>假设现在有一个名为 <code>createAudioFileAsync()</code> 的函数,它接收一些配置和两个回调函数,然后异步地生成音频文件。一个回调函数在文件成功创建时被调用,另一个则在出现异常时被调用。</p>
<p>以下为使用 <code>createAudioFileAsync()</code> 的示例:</p>
<pre class="brush: js">// 成功的回调函数
function successCallback(result) {
console.log("音频文件创建成功: " + result);
}
// 失败的回调函数
function failureCallback(error) {
console.log("音频文件创建失败: " + error);
}
createAudioFileAsync(audioSettings, successCallback, failureCallback)</pre>
<p>更现代的函数会返回一个 Promise 对象,使得你可以将你的回调函数绑定在该 Promise 上。</p>
<p>如果函数 <code>createAudioFileAsync()</code> 被重写为返回 Promise 的形式,那么我们可以像下面这样简单地调用它:</p>
<pre class="brush: js line-numbers language-js">const promise = createAudioFileAsync(audioSettings);
promise.then(successCallback, failureCallback);
</pre>
<p>或者简写为:</p>
<pre class="brush: js line-numbers language-js">createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
</pre>
<p>我们把这个称为 <em>异步函数调用</em>,这种形式有若干优点,下面我们将会逐一讨论。</p>
<h2 id="约定">约定</h2>
<p>不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:</p>
<ul>
<li>在本轮 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop#执行至完成">事件循环</a> 运行完成之前,回调函数是不会被调用的。</li>
<li>即使异步操作已经完成(成功或失败),在这之后通过 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then"><code>then()</code> </a>添加的回调函数也会被调用。</li>
<li>通过多次调用 <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then">then()</a></code> 可以添加多个回调函数,它们会按照插入顺序进行执行。</li>
</ul>
<p>Promise 很棒的一点就是<strong>链式调用</strong>(<strong>chaining</strong>)。</p>
<h2 id="链式调用">链式调用</h2>
<p>连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个 <strong>Promise 链</strong>来实现这种需求。</p>
<p>见证奇迹的时刻:<code>then()</code> 函数会返回一个和原来不同的<strong>新的 Promise</strong>:</p>
<pre class="brush: js line-numbers language-js">const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
</pre>
<p>或者</p>
<pre class="brush: js">const promise2 = doSomething().then(successCallback, failureCallback);</pre>
<p><code>promise2</code> 不仅表示 <code>doSomething()</code> 函数的完成,也代表了你传入的 <code>successCallback</code> 或者 <code>failureCallback</code> 的完成,这两个函数也可以返回一个 Promise 对象,从而形成另一个异步操作,这样的话,在 <code>promise2</code> 上新增的回调函数会排在这个 Promise 对象的后面。</p>
<p>基本上,每一个 Promise 都代表了链中另一个异步过程的完成。</p>
<p>在过去,要想做多重的异步操作,会导致经典的回调地狱:</p>
<pre class="brush: js">doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
</pre>
<p>现在,我们可以把回调绑定到返回的 Promise 上,形成一个 Promise 链:</p>
<pre class="brush: js">doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
</pre>
<p>then 里的参数是可选的,<code>catch(failureCallback)</code> 是 <code>then(null, failureCallback)</code> 的缩略形式。如下所示,我们也可以用<a href="/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions">箭头函数</a>来表示:</p>
<pre class="brush: js">doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
</pre>
<p><strong>注意:</strong>一定要有返回值,否则,callback 将无法获取上一个 Promise 的结果。(如果使用箭头函数,<code>() => x</code> 比 <code>() => { return x; }</code> 更简洁一些,但后一种保留 <code>return</code> 的写法才支持使用多个语句。)。</p>
<h3 id="Catch_的后续链式操作">Catch 的后续链式操作</h3>
<p>有可能会在一个回调失败之后继续使用链式操作,即,使用一个 <code>catch</code>,这对于在链式操作中抛出一个失败之后,再次进行新的操作会很有用。请阅读下面的例子:</p>
<pre class="brush: js">new Promise((resolve, reject) => {
console.log('初始化');
resolve();
})
.then(() => {
throw new Error('有哪里不对了');
console.log('执行「这个」”');
})
.catch(() => {
console.log('执行「那个」');
})
.then(() => {
console.log('执行「这个」,无论前面发生了什么');
});
</pre>
<p>输出结果如下:</p>
<pre>初始化
执行“那个”
执行“这个”,无论前面发生了什么
</pre>
<p><strong>注意:</strong>因为抛出了错误 <u>有哪里不对了</u>,所以前一个 <u>执行「这个」</u> 没有被输出。</p>
<h2 id="错误传递">错误传递</h2>
<p>在之前的回调地狱示例中,你可能记得有 3 次 <code>failureCallback</code> 的调用,而在 Promise 链中只有尾部的一次调用。</p>
<pre class="brush: js">doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
</pre>
<p>通常,一遇到异常抛出,浏览器就会顺着 Promise 链寻找下一个 <code>onRejected</code> 失败回调函数或者由 <code>.catch()</code> 指定的回调函数。这和以下同步代码的工作原理(执行过程)非常相似。</p>
<pre class="brush: js">try {
let result = syncDoSomething();
let newResult = syncDoSomethingElse(result);
let finalResult = syncDoThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
</pre>
<p>在 ECMAScript 2017 标准的 <code><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function">async/await</a></code> 语法糖中,这种异步代码的对称性得到了极致的体现:</p>
<pre class="brush: js">async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
}</pre>
<p>这个例子是在 Promise 的基础上构建的,例如,<code>doSomething()</code> 与之前的函数是相同的。你可以在<a href="https://developers.google.com/web/fundamentals/getting-started/primers/async-functions">这里</a>阅读更多的与此语法相关的文章。</p>
<p>通过捕获所有的错误,甚至抛出异常和程序错误,Promise 解决了回调地狱的基本缺陷。这对于构建异步操作的基础功能而言是很有必要的。</p>
<h2 id="Promise_拒绝事件">Promise 拒绝事件</h2>
<p>当 Promise 被拒绝时,会有下文所述的两个事件之一被派发到全局作用域(通常而言,就是{{domxref("window")}};如果是在 web worker 中使用的话,就是 {{domxref("Worker")}} 或者其他 worker-based 接口)。这两个事件如下所示:</p>
<dl>
<dt>{{domxref("Window.rejectionhandled_event", "rejectionhandled")}}</dt>
<dd>当 Promise 被拒绝、并且在 <code>reject</code> 函数处理该 rejection 之后会派发此事件。</dd>
<dt>{{domxref("Window.unhandledrejection_event", "unhandledrejection")}}</dt>
<dd>当 Promise 被拒绝,但没有提供 <code>reject</code> 函数来处理该 rejection 时,会派发此事件。</dd>
</dl>
<p>以上两种情况中,{{domxref("PromiseRejectionEvent")}} 事件都有两个属性,一个是 {{domxref("PromiseRejectionEvent.promise", "promise")}} 属性,该属性指向被驳回的 Promise,另一个是 {{domxref("PromiseRejectionEvent.reason", "reason")}} 属性,该属性用来说明 Promise 被驳回的原因。</p>
<p>因此,我们可以通过以上事件为 Promise 失败时提供补偿处理,也有利于调试 Promise 相关的问题。在每一个上下文中,该处理都是全局的,因此不管源码如何,所有的错误都会在同一个处理函数中被捕捉并处理。</p>
<p>一个特别有用的例子:当你使用 {{Glossary("Node.js")}} 时,有些依赖模块可能会有未被处理的 rejected promises,这些都会在运行时打印到控制台。你可以在自己的代码中捕捉这些信息,然后添加与 {{domxref("Window.unhandledrejection_event", "unhandledrejection")}} 相应的处理函数来做分析和处理,或只是为了让你的输出更整洁。举例如下:</p>
<pre class="brush: js">window.addEventListener("unhandledrejection", event => {
/* 你可以在这里添加一些代码,以便检查
event.promise 中的 promise 和
event.reason 中的 rejection 原因 */
event.preventDefault();
}, false);
</pre>
<p>调用 event 的 {{domxref("Event.preventDefault", "preventDefault()")}} 方法是为了告诉 JavaScript 引擎当 Promise 被拒绝时不要执行默认操作,默认操作一般会包含把错误打印到控制台,Node 就是如此的。</p>
<p>理想情况下,在忽略这些事件之前,我们应该检查所有被拒绝的 Promise,来确认这不是代码中的 bug。</p>
<h2 id="在旧式回调_API_中创建_Promise">在旧式回调 API 中创建 Promise</h2>
<p>可以通过 Promise 的构造器从零开始创建 {{jsxref("Promise")}}。 这种方式(通过构造器的方式)应当只在封装旧 API 的时候用到。</p>
<p>理想状态下,所有的异步函数都已经返回 Promise 了。但有一些 API 仍然使用旧方式来传入的成功(或者失败)的回调。典型的例子就是 {{domxref("WindowTimers.setTimeout", "setTimeout()")}} 函数:</p>
<pre class="brush: js">setTimeout(() => saySomething("10 seconds passed"), 10000);
</pre>
<p>混用旧式回调和 Promise 可能会造成运行时序问题。如果 <code>saySomething</code> 函数失败了,或者包含了编程错误,那就没有办法捕获它了。这得怪 <code>setTimeout</code>。</p>
<p>幸运地是,我们可以用 Promise 来封装它。最好的做法是,将这些有问题的函数封装起来,留在底层,并且永远不要再直接调用它们:</p>
<pre class="brush: js">const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);
</pre>
<p>通常,Promise 的构造器接收一个执行函数(executor),我们可以在这个执行函数里手动地 resolve 和 reject 一个 Promise。既然 <code>setTimeout</code> 并不会真的执行失败,那么我们可以在这种情况下忽略 reject。</p>
<h2 id="组合">组合</h2>
<p>{{jsxref("Promise.resolve()")}} 和 {{jsxref("Promise.reject()")}} 是手动创建一个已经 resolve 或者 reject 的 Promise 快捷方法。它们有时很有用。</p>
<p>{{jsxref("Promise.all()")}} 和 {{jsxref("Promise.race()")}} 是并行运行异步操作的两个组合式工具。</p>
<p>我们可以发起并行操作,然后等多个操作全部结束后进行下一步操作,如下:</p>
<pre class="brush: js">Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });</pre>
<p>可以使用一些聪明的 JavaScript 写法实现时序组合:</p>
<pre class="brush: js">[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* use result3 */ });</pre>
<p>通常,我们递归调用一个由异步函数组成的数组时,相当于一个 Promise 链:</p>
<pre><code>Promise.resolve().then(func1).then(func2).then(func3);</code></pre>
<p>我们也可以写成可复用的函数形式,这在函数式编程中极为普遍:</p>
<pre class="brush: js">const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
</pre>
<p><code>composeAsync()</code> 函数将会接受任意数量的函数作为其参数,并返回一个新的函数,该函数接受一个通过 composition pipeline 传入的初始值。这对我们来说非常有益,因为任一函数可以是异步或同步的,它们能被保证按顺序执行:</p>
<pre class="brush: js">const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);
</pre>
<p>在 ECMAScript 2017 标准中, 时序组合可以通过使用 <code>async/await</code> 而变得更简单:</p>
<pre class="brush: js">let result;
for (const f of [func1, func2, func3]) {
result = await f(result);
}
/* use last result (i.e. result3) */</pre>
<h2 id="时序">时序</h2>
<p>为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 <code><a href="/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/then">then()</a></code> 的函数也总是会被异步调用:</p>
<pre class="brush: js">Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
</pre>
<p>传递到 <code>then()</code> 中的函数被置入到一个微任务队列中,而不是立即执行,这意味着它是在 JavaScript 事件队列的所有运行时结束了,且事件队列被清空之后,才开始执行:</p>
<pre class="brush: js">const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4</pre>
<h2 id="嵌套">嵌套</h2>
<p>简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise,因为嵌套经常会是粗心导致的。可查阅下一节的<a href="#常见错误">常见错误</a>中的例子。</p>
<p>嵌套 Promise 是一种可以限制 <code>catch</code> 语句的作用域的控制结构写法。明确来说,嵌套的 <code>catch</code> 仅捕捉在其之前同时还必须是其作用域的 failureres,而捕捉不到在其链式以外或者其嵌套域以外的 error。如果使用正确,那么可以实现高精度的错误修复。</p>
<pre class="brush: js">doSomethingCritical()
.then(result => doSomethingOptional()
.then(optionalResult => doSomethingExtraNice(optionalResult))
.catch(e => {console.log(e.message)})) // 即使有异常也会忽略,继续运行;(最后会输出)
.then(() => moreCriticalStuff())
.catch(e => console.log("Critical failure: " + e.message));// 没有输出</pre>
<p>注意,有些代码步骤是嵌套的,而不是一个简单的纯链式,这些语句前与后都被括号 <code>()</code> 包裹着。</p>
<p>这个内部的 <code>catch</code> 语句仅能捕获到 <code>doSomethingOptional()</code> 和 <code>doSomethingExtraNice()</code> 的失败,<code>之后就恢复到moreCriticalStuff()</code> 的运行。重要提醒:如果 <code>doSomethingCritical()</code> 失败,这个错误仅会被最后的(外部)<code>catch</code> 语句捕获到。</p>
<h2 id="常见错误">常见错误</h2>
<p>在编写 Promise 链时,需要注意以下示例中展示的几个错误:</p>
<pre class="brush: js example-bad">// 错误示例,包含 3 个问题!
doSomething().then(function(result) {
doSomethingElse(result) // 没有返回 Promise 以及没有必要的嵌套 Promise
.then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// 最后,是没有使用 catch 终止 Promise 调用链,可能导致没有捕获的异常</pre>
<p>第一个错误是没有正确地将事物相连接。当我们创建新 Promise 但忘记返回它时,会发生这种情况。因此,链条被打破,或者更确切地说,我们有两个独立的链条竞争(同时在执行两个异步而非一个一个的执行)。这意味着 <code>doFourthThing()</code> 不会等待 <code>doSomethingElse()</code> 或 <code>doThirdThing()</code> 完成,并且将与它们并行运行,可能是无意的。单独的链也有单独的错误处理,导致未捕获的错误。</p>
<p>第二个错误是不必要地嵌套,实现第一个错误。嵌套还限制了内部错误处理程序的范围,如果是非预期的,可能会导致未捕获的错误。其中一个变体是 <a href="https://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it">Promise 构造函数反模式</a>,它结合了 Promise 构造函数的多余使用和嵌套。</p>
<p>第三个错误是忘记用 <code>catch</code> 终止链。这导致在大多数浏览器中不能终止的 Promise 链里的 rejection。</p>
<p>一个好的经验法则是总是返回或终止 Promise 链,并且一旦你得到一个新的 Promise,返回它。下面是修改后的平面化的代码:</p>
<pre class="brush: js example-good">doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));</pre>
<p>注意:<code>() => x</code> 是 <code>() => { return x; }</code> 的简写。</p>
<p>上述代码的写法就是具有适当错误处理的简单明确的链式写法。</p>
<p>使用 <a href="/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function">async/await</a> 可以解决以上大多数错误,使用 <code>async/await</code> 时,最常见的语法错误就是忘记了 <code><a href="/zh-CN/docs/Web/JavaScript/Reference/Operators/await">await</a></code> 关键字。</p>
<h2 id="参见">参见</h2>
<ul>
<li>{{jsxref("Promise.then()")}}</li>
<li><a href="/en-US/docs/Web/JavaScript/Reference/Statements/async_function"><code>async</code>/<code>await</code></a></li>
<li><a href="https://promisesaplus.com/">Promises/A+ specification</a></li>
<li><a href="https://medium.com/@ramsunvtech/promises-of-promise-part-1-53f769245a53">Venkatraman.R - JS Promise (Part 1, Basics)</a></li>
<li><a href="https://medium.com/@ramsunvtech/js-promise-part-2-q-js-when-js-and-rsvp-js-af596232525c">Venkatraman.R - JS Promise (Part 2 - Using Q.js, When.js and RSVP.js)</a></li>
<li><a href="https://tech.io/playgrounds/11107/tools-for-promises-unittesting/introduction">Venkatraman.R - Tools for Promises Unit Testing</a></li>
<li><a href="http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html">Nolan Lawson: We have a problem with promises — Common mistakes with promises</a></li>
</ul>
<p>{{PreviousNext("Web/JavaScript/Guide/Details_of_the_Object_Model", "Web/JavaScript/Guide/Iterators_and_Generators")}}</p>
|