aboutsummaryrefslogtreecommitdiff
path: root/files/zh-cn/learn/javascript/asynchronous/promises/index.html
blob: 665bda8129cf68728d2fd7a4557ca8cab30588a1 (plain)
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
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
---
title: 优雅的异步处理
slug: learn/JavaScript/异步/Promises语法
translation_of: Learn/JavaScript/Asynchronous/Promises
---
<div>{{LearnSidebar}}</div>

<div>{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Timeouts_and_intervals", "Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}</div>

<p class="summary"><strong>Promise</strong> 是 JavaScript 语言的一个相对较新的功能,允许你推迟进一步的操作,直到上一个操作完成或响应其失败。这对于设置一系列异步操作以正常工作非常有用。本文向你展示了promises如何工作,如何在Web API中使用它们以及如何编写自己的API</p>

<table class="learn-box standard-table">
 <tbody>
  <tr>
   <th scope="row">前提条件:</th>
   <td>基本的计算机素养,具备基础的JavaScript知识</td>
  </tr>
  <tr>
   <th scope="row">目标:</th>
   <td>理解并使用学习如何使用Promises</td>
  </tr>
 </tbody>
</table>

<h2 id="什么是promises">什么是promises?</h2>

<p>我们在教程的第一篇文章中简要地了解了 <a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promises</a>,接下来我们将在更深层次理解Promise。</p>

<p>本质上,Promise 是一个对象,代表操作的中间状态 —— 正如它的单词含义 '承诺' ,它保证在未来可能返回某种结果。虽然 Promise 并不保证操作在何时完成并返回结果,但是它保证当结果可用时,你的代码能正确处理结果,当结果不可用时,你的代码同样会被执行,来优雅的处理错误。</p>

<p>通常你不会对一个异步操作从开始执行到返回结果所用的时间感兴趣(除非它耗时过长),你会更想在任何时候都能响应操作结果,当然它不会阻塞其余代码的执行就更好了。</p>

<p>你与 Promise 常见的交互之一就是 Web API 返回的 promise 对象。让我们设想一个视频聊天应用程序,该程序有一个展示用户的朋友列表的窗口,可以点击朋友旁边的按钮对朋友视频呼叫。</p>

<p>该按钮的处理程序调用 {{domxref("MediaDevices.getUserMedia", "getUserMedia()")}} 来访问用户的摄像头和麦克风。由于 <code>getUserMedia()</code> 必须确保用户具有使用这些设备的权限,并询问用户要使用哪个麦克风和摄像头(或者是否仅进行语音通话,以及其他可能的选项),因此它会产生阻塞,直到用户做出所有的决定,并且摄像头和麦克风都已启用。此外,用户可能不会立即响应权限请求。所以 <code>getUserMedia()</code> 可能需要很长时间。</p>

<p>由于 <code>getUserMedia()</code> 是在浏览器的主线程进行调用,整个浏览器将会处于阻塞状态直到 <code>getUserMedia()</code> 返回,这是不应该发生的;不使用Promise,浏览器将处于不可用状态直到用户为摄像头和麦克风做出决定。因此 <code>getUserMedia()</code> 返回一个Promise对象,即 {{jsxref("promise")}},一旦  {{domxref("MediaStream")}} 流可用才去解析,而不是等待用户操作、启动选中的设备并直接返回从所选资源创建的 {{domxref("MediaStream")}} 流。</p>

<p>上述视频聊天应用程序的代码可能像下面这样:</p>

<pre class="brush: js notranslate">function handleCallButton(evt) {
  setStatusMessage("Calling...");
  navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then(chatStream =&gt; {
      selfViewElem.srcObject = chatStream;
      chatStream.getTracks().forEach(track =&gt; myPeerConnection.addTrack(track, chatStream));
      setStatusMessage("Connected");
    }).catch(err =&gt; {
      setStatusMessage("Failed to connect");
    });
}
</pre>

<p>这个函数在开头调用 <code>setStatusMessage()</code> 来更新状态显示信息"Calling...", 表示正在尝试通话。接下来调用 <code>getUserMedia()</code>,请求具有视频及音频轨的流,一旦获得这个流,就将其显示在"selfViewElem"的video元素中。接下来将这个流的每个轨道添加到表示与另一个用户的连接的 <a href="/en-US/docs/Web/API/WebRTC_API">WebRTC</a>,参见{{domxref("RTCPeerConnection")}}。在这之后,状态显示为"Connected"。</p>

<p>如果<code>getUserMedia()</code>失败,则catch块运行。这使用<code>setStatusMessage()</code>更新状态框以指示发生错误。</p>

<p>这里重要的是<code>getUserMedia()</code>调用几乎立即返回,即使尚未获得相机流。即使<code>handleCallButton()</code>函数向调用它的代码返回结果,当<code>getUserMedia()</code>完成工作时,它也会调用你提供的处理程序。只要应用程序不假设流式传输已经开始,它就可以继续运行。</p>

<div class="blockIndicator note">
<p><strong>注意:</strong>  如果你有兴趣,可以在文章<a href="/docs/Web/API/WebRTC_API/Signaling_and_video_calling">Signaling and video calling</a>中了解有关此高级主题的更多信息。在该示例中使用与此类似的代码,但更完整。</p>
</div>

<h2 id="回调函数的麻烦"> 回调函数的麻烦</h2>

<p>要完全理解为什么 promise 是一件好事,应该回想之前的回调函数,理解它们造成的困难。</p>

<p>我们来谈谈订购披萨作为类比。为了使你的订单成功,你必须按顺序执行,不按顺序执行或上一步没完成就执行下一步是不会成功的:</p>

<ol>
 <li>选择配料。如果你是优柔寡断,这可能需要一段时间,如果你无法下定决心或者决定换咖喱,可能会失败。</li>
 <li>下订单。返回比萨饼可能需要一段时间,如果餐厅没有烹饪所需的配料,可能会失败。</li>
 <li>然后你收集你的披萨吃。如果你忘记了自己的钱包,那么这可能会失败,所以无法支付比萨饼的费用!</li>
</ol>

<p>对于旧式<a href="/en-US/docs/Learn/JavaScript/Asynchronous/Introducing#Callbacks">callbacks</a>,上述功能的伪代码表示可能如下所示:</p>

<pre class="brush: js notranslate">chooseToppings(function(toppings) {
  placeOrder(toppings, function(order) {
    collectOrder(order, function(pizza) {
      eatPizza(pizza);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);</pre>

<p>这很麻烦且难以阅读(通常称为“回调地狱”),需要多次调用<code>failureCallback()</code>(每个嵌套函数一次),还有其他问题。</p>

<h3 id="使用promise改良">使用promise改良</h3>

<p>Promises使得上面的情况更容易编写,解析和运行。如果我们使用异步promises代表上面的伪代码,我们最终会得到这样的结果:</p>

<pre class="brush: js notranslate">chooseToppings()
.then(function(toppings) {
  return placeOrder(toppings);
})
.then(function(order) {
  return collectOrder(order);
})
.then(function(pizza) {
  eatPizza(pizza);
})
.catch(failureCallback);</pre>

<p>这要好得多 - 更容易看到发生了什么,我们只需要一个<code>.catch()</code>块来处理所有错误,它不会阻塞主线程(所以我们可以在等待时继续玩视频游戏为了准备好收集披萨),并保证每个操作在运行之前等待先前的操作完成。我们能够以这种方式一个接一个地链接多个异步操作,因为每个<code>.then()</code>块返回一个新的promise,当<code>.then()</code>块运行完毕时它会解析。聪明,对吗?</p>

<p>使用箭头函数,你可以进一步简化代码:</p>

<pre class="brush: js notranslate">chooseToppings()
.then(toppings =&gt;
  placeOrder(toppings)
)
.then(order =&gt;
  collectOrder(order)
)
.then(pizza =&gt;
  eatPizza(pizza)
)
.catch(failureCallback);</pre>

<p>甚至这样:</p>

<pre class="brush: js notranslate">chooseToppings()
.then(toppings =&gt; placeOrder(toppings))
.then(order =&gt; collectOrder(order))
.then(pizza =&gt; eatPizza(pizza))
.catch(failureCallback);</pre>

<p>这是有效的,因为使用箭头函数 <code>() =&gt; x</code><code>()=&gt; {return x;}</code>  的有效简写; 。</p>

<p>你甚至可以这样做,因为函数只是直接传递它们的参数,所以不需要额外的函数层:</p>

<pre class="brush: js notranslate">chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback);</pre>

<p>但是,这并不容易阅读,如果你的块比我们在此处显示的更复杂,则此语法可能无法使用。</p>

<div class="blockIndicator note">
<p><strong>注意</strong>: 你可以使用 <code>async/await</code> 语法进行进一步的改进,我们将在下一篇文章中深入讨论。</p>
</div>

<p>最基本的,promise与事件监听器类似,但有一些差异:</p>

<ul>
 <li>一个promise只能成功或失败一次。它不能成功或失败两次,并且一旦操作完成,它就无法从成功切换到失败,反之亦然。</li>
 <li>如果promise成功或失败并且你稍后添加成功/失败回调,则将调用正确的回调,即使事件发生在较早的时间。</li>
</ul>

<h2 id="解释promise的基本语法:一个真实的例子"> 解释promise的基本语法:一个真实的例子</h2>

<p>Promises 很重要,因为大多数现代Web API都将它们用于执行潜在冗长任务的函数。要使用现代Web技术,你需要使用promises。在本章的后面我们将看看如何编写自己的promises,但是现在我们将看一些你将在Web API中遇到的简单示例。</p>

<p>在第一个示例中,我们将使用<code>fetch()</code>方法从Web获取图像,<code>blob()</code> 方法来转换获取响应的原始内容到 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a> 对象,然后在 <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img">&lt;img&gt; </a>元素内显示该<code>blob</code>。这与我们在 <a href="/en-US/docs/Learn/JavaScript/Asynchronous/Introducing#Asynchronous_JavaScript">first article of the series</a>中看到的示例非常相似,但是会在构建你自己的基于 promise 的代码时有所不同。 </p>

<div class="blockIndicator note">
<p><strong>注意: </strong>下列代码无法直接运行(i.e. via a <code>file://</code> URL)。你需要<a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server">本地测试服务器</a>,或是 <a href="https://glitch.com/">Glitch</a><a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Using_Github_pages">GitHub pages</a> 这样的在线解决方案。</p>
</div>

<ol>
 <li>
  <p>首先,下载我们的 <a href="https://github.com/mdn/learning-area/blob/master/html/introduction-to-html/getting-started/index.html">simple HTML template</a><a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/coffee.jpg">sample image file</a></p>
 </li>
 <li>
  <p>将 {{htmlelement("script")}} 元素添加在HTML {{htmlelement("body")}} 的底部。</p>
 </li>
 <li>
  <p>在 {{HTMLElement("script")}} 元素内,添加以下行:</p>

  <pre class="brush: js notranslate">let promise = fetch('coffee.jpg');</pre>

  <p>这会调用 <code>fetch()</code> 方法,将图像的URL作为参数从网络中提取。这也可以将options对象作为可选的第二个参数,但我们现在只使用最简单的版本。我们将 <code>fetch()</code> 返回的promise对象存储在一个名为promise的变量中。正如我们之前所说的,这个对象代表了一个最初既不成功也不失败的中间状态 - 这个状态下的promise的官方术语叫作<strong>pending</strong></p>
 </li>
 <li>为了响应成功完成操作(在这种情况下,当返回{{domxref("Response")}}时),我们调用promise对象的.<code>then()</code>方法。 <code>.then()</code>块中的回调(称为执行程序)仅在promise调用成功完成时运行并返回{{domxref("Response")}}对象 - 在promise-speak中,当它已被满足时。它将返回的{{domxref("Response")}}对象作为参数传递。</li>
</ol>

<div class="blockIndicator note">
<p><strong>注意</strong>: <code>.then()</code>块的工作方式类似于使用<code>AddEventListener()</code>向对象添加事件侦听器时的方式。它不会在事件发生之前运行(当promise履行时)。最显着的区别是<code>.then()</code>每次使用时只运行一次,而事件监听器可以多次调用。</p>
</div>

<p>我们立即对此响应运行<code>blob()</code>方法以确保响应主体完全下载,并且当它可用时将其转换为我们可以执行某些操作的<code>Blob</code>对象。返回的结果如下:</p>

<pre class="brush: js notranslate">response =&gt; response.blob()</pre>

<p>这是下面的简写</p>

<pre class="brush: js notranslate">function(response) {
    return response.blob();
}
</pre>

<p>好的,我们还需要做点额外的工作。Fetch promises 不会产生 404 或 500错误,只有在产生像网路故障的情况时才会不工作。总的来说,Fetch promises 总是成功运行,即使<a href="https://developer.mozilla.org/en-US/docs/Web/API/Response/ok">response.ok</a> 属性是<code> false</code>。为了产生404错误,我们需要判断 <code>response.ok</code> ,如果是 <code>false</code>,抛出错误,否则返回 blob。就像下面的代码这样做。</p>

<pre class="brush: js notranslate"><code>let promise2 = promise.then(response =&gt; {
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return response.blob();
  }
});</code></pre>

<p>5. 每次调用<code>.then()</code>都会创建一个新的promise。这非常有用;因为<code>blob()</code>方法也返回一个promise,我们可以通过调用第二个promise的<code>.then()</code>方法来处理它在履行时返回的<code>Blob</code>对象。因为我们想要对<code>blob</code>执行一些更复杂的操作,而不仅仅运行单个方法并返回结果,这次我们需要将函数体包装成花括号(否则会抛出错误)。</p>

<p>将以下内容添加到代码的末尾:</p>

<pre class="brush: js notranslate">let promise3 = promise2.then(myBlob =&gt; {})</pre>

<p>6. 现在让我们填写执行程序函数的主体。在花括号内添加以下行:</p>

<pre class="brush: js notranslate"><code>let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);</code></pre>

<p>这里我们运行{{domxref("URL.createObjectURL()")}}方法,将其作为<code>Blob</code>在第二个promise实现时返回的参数传递。这将返回指向该对象的URL。然后我们创建一个{{htmlelement("img")}}元素,将其<code>src</code>属性设置为等于对象URL并将其附加到DOM,这样图像就会显示在页面上!</p>

<p>如果你保存刚刚创建的HTML文件并将其加载到浏览器中,你将看到图像按预期显示在页面中。干得好!</p>

<div class="blockIndicator note">
<p><strong>注意</strong>: 你可能会注意到这些例子有点做作。你可以取消整个<code>fetch()</code><code>blob()</code>链,只需创建一个&lt;img&gt;元素并将其<code>src</code>属性值设置为图像文件的URL,即<code>coffee.jpg</code>。然而,我们选择了这个例子,因为它以简单的方式展示了promise,而不是真实世界的适当性。</p>
</div>

<h3 id="响应失败">响应失败</h3>

<p>缺少一些东西 - 如果其中一个promise失败(<strong>rejects</strong>,in promise-speak),目前没有什么可以明确地处理错误。我们可以通过运行前一个promise的 <code>.catch()</code> 方法来添加错误处理。立即添加:</p>

<pre class="brush: js line-numbers language-js notranslate"><code class="language-js"><span class="keyword token">let</span> errorCase <span class="operator token">=</span> promise3<span class="punctuation token">.</span><span class="function token">catch</span><span class="punctuation token">(</span><span class="parameter token">e</span> <span class="operator token">=&gt;</span> <span class="punctuation token">{</span>
  console<span class="punctuation token">.</span><span class="function token">log</span><span class="punctuation token">(</span><span class="string token">'There has been a problem with your fetch operation: '</span> <span class="operator token">+</span> e<span class="punctuation token">.</span>message<span class="punctuation token">)</span><span class="punctuation token">;</span>
<span class="punctuation token">}</span><span class="punctuation token">)</span><span class="punctuation token">;</span></code></pre>

<p>要查看此操作,请尝试拼错图像的URL并重新加载页面。该错误将在浏览器的开发人员工具的控制台中报告。</p>

<p>如果你根本不操心包括的 <code>.catch()</code> 块,这并没有做太多的事情,但考虑一下(指.catch()块) ––这会使我们可以完全控制错误处理方式。在真实的应用程序中,你的<code>.catch()</code>块可以重试获取图像,或显示默认图像,或提示用户提供不同的图像URL等等。</p>

<div class="blockIndicator note">
<p><strong>注意</strong>: 你可以参考 <a href="https://mdn.github.io/learning-area/javascript/asynchronous/promises/simple-fetch.html">our version of the example live</a> (参阅 <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/simple-fetch.html">source code</a> ).</p>
</div>

<h3 id="将代码块链在一起">将代码块链在一起</h3>

<p>这是写出来的一种非常简便的方式;我们故意这样做是为了帮助你清楚地了解发生了什么。如本文前面所示,你可以将<code>.then()</code>块(以及<code>.catch()</code>块)链接在一起。上面的代码也可以这样写(参阅GitHub上的<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/simple-fetch-chained.html">simple-fetch-chained.html</a> ):</p>

<pre class="brush: js notranslate"><code>fetch('coffee.jpg')
.then(response =&gt; {
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return response.blob();
  }
})
.then(myBlob =&gt; {
  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
})
.catch(e =&gt; {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});</code></pre>

<p>请记住,履行的promise所返回的值将成为传递给下一个 <code>.then()</code> 块的executor函数的参数。</p>

<div class="blockIndicator note">
<p><strong>注意</strong>: promise中的<code>.then()/catch()</code>块基本上是同步代码中<code><a href="/en-US/docs/Web/JavaScript/Reference/Statements/try...catch">try...catch</a></code>块的异步等价物。请记住,同步<code>try ... catch</code>在异步代码中不起作用。</p>
</div>

<h2 id="Promise术语回顾">Promise术语回顾</h2>

<p>在上面的部分中有很多要介绍的内容,所以让我们快速回过头来给你<a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Promises#Promise_terminology_recap">一个简短的指南</a>,你可以将它添加到书签中,以便将来更新你的记忆。你还应该再次阅读上述部分,以确保这些概念坚持下去。</p>

<ol>
 <li>创建promise时,它既不是成功也不是失败状态。这个状态叫作<strong>pending</strong>(待定)。</li>
 <li>当promise返回时,称为 <strong>resolved</strong>(已解决).
  <ol>
   <li>一个成功<strong>resolved</strong>的promise称为<strong>fullfilled</strong><strong>实现</strong>)。它返回一个值,可以通过将<code>.then()</code>块链接到promise链的末尾来访问该值。<code> .then()</code>块中的执行程序函数将包含promise的返回值。</li>
   <li>一个不成功<strong>resolved</strong>的promise被称为<strong>rejected</strong><strong>拒绝</strong>)了。它返回一个原因(<strong>reason</strong>),一条错误消息,说明为什么拒绝promise。可以通过将<code>.catch()</code>块链接到promise链的末尾来访问此原因。</li>
  </ol>
 </li>
</ol>

<h2 id="运行代码以响应多个Promises的实现"> 运行代码以响应多个Promises的实现</h2>

<p>上面的例子向我们展示了使用promises的一些真正的基础知识。现在让我们看一些更高级的功能。首先,链接进程一个接一个地发生都很好,但是如果你想在一大堆Promises全部完成之后运行一些代码呢?</p>

<p> 你可以使用巧妙命名的<a href="/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">Promise.all()</a>静态方法完成此操作。这将一个promises数组作为输入参数,并返回一个新的Promise对象,只有当数组中的所有promise都满足时才会满足。它看起来像这样:</p>

<pre class="brush: js notranslate">Promise.all([a, b, c]).then(values =&gt; {
  ...
});</pre>

<p>如果它们都<strong>实现</strong>,那么数组中的结果将作为参数传递给<code>.then()</code>块中的执行器函数。如果传递给<code>Promise.all()</code>的任何一个 promise <strong>拒绝</strong>,整个块将<strong>拒绝</strong></p>

<p>这非常有用。想象一下,我们正在获取信息以在内容上动态填充页面上的UI功能。在许多情况下,接收所有数据然后才显示完整内容,而不是显示部分信息。</p>

<p>让我们构建另一个示例来展示这一点。</p>

<ol>
 <li>
  <p>下载我们页面模板(<a href="https://github.com/mdn/learning-area/blob/master/html/introduction-to-html/getting-started/index.html">page template</a>)的新副本,并再次在结束&lt;/ body&gt;标记之前放置一个&lt;script&gt;元素。</p>
 </li>
 <li>
  <p>下载我们的源文件(<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/coffee.jpg">coffee.jpg</a>, <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/tea.jpg">tea.jpg</a>和 <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/description.txt">description.txt</a>),或者随意替换成你自己的文件。</p>
 </li>
 <li>
  <p>在我们的脚本中,我们将首先定义一个函数,该函数返回我们要发送给<code>Promise.all()</code>的promise。如果我们只想运行<code>Promise.all()</code>块以响应三个<code>fetch()</code>操作完成,这将很容易。我们可以这样做:</p>

  <pre class="brush: js notranslate">let a = fetch(url1);
let b = fetch(url2);
let c = fetch(url3);

Promise.all([a, b, c]).then(values =&gt; {
  ...
});</pre>

  <p>当promise是<strong>fullfilled</strong>时,传递到履行处理程序的<code>values</code>将包含三个Response对象,每个对象用于已完成的每个<code>fetch()</code>操作。</p>

  <p>但是,我们不想这样做。我们的代码不关心<code>fetch()</code>操作何时完成。相反,我们想要的是加载的数据。这意味着当我们返回代表图像的可用<code>blob</code>和可用的文本字符串时,我们想要运行<code>Promise.all()</code>块。我们可以编写一个执行此操作的函数;在<code>&lt;script&gt;</code>元素中添加以下内容:</p>

  <pre class="brush: js notranslate">function fetchAndDecode(url, type) {
  return fetch(url).then(response =&gt; {
    if (type === 'blob') {
      return response.blob();
    } else if (type === 'text') {
      return response.text();
    }
  })
  .catch(e =&gt; {
    console.log('There has been a problem with your fetch operation: ' + e.message);
  });
}</pre>

  <p>这看起来有点复杂,所以让我们一步一步地完成它:</p>

  <ol>
   <li>首先,我们定义函数,向它传递一个URL和字符串,这个字符串表示资源类型。</li>
   <li>在函数体内部,我们有一个类似于我们在第一个例子中看到的结构 - 我们调用<code>fetch()</code>函数来获取指定URL处的资源,然后将其链接到另一个 promise ,它解码(或“read”)响应body。这是前一个示例中的<code>blob()</code>方法。</li>
   <li>但是,这里有两处不同:
    <ul>
     <li>首先,我们返回的第二个promise会因类型值的不同而不同。在执行函数内部,我们包含一个简单的<code>if ... else if</code>语句,根据我们需要解码的文件类型返回不同的promise(在这种情况下,我们可以选择<code>blob</code><code>text</code>,而且很容易扩展这个以处理其他类型)。</li>
     <li>其次,我们在<code>fetch()</code>调用之前添加了<code>return</code>关键字。它的作用是运行整个链,然后运行最终结果(即<code>blob()</code><code>text()</code>返回的promise作为我们刚刚定义的函数的返回值)。实际上,<code>return</code>语句将结果从链返回到顶部。</li>
    </ul>
   </li>
   <li>
    <p>在块结束时,我们链接一个<code>.catch()</code>调用,以处理任何可能出现在数组中传递给<code>.all()</code>的任何promise的错误情况。如果任何promise被拒绝,<code>catch</code>块将告诉你哪个promise有问题。 <code>.all()</code>块(见下文)仍然可以实现,但不会显示有问题的资源。如果你想要<code>.all</code>拒绝,你必须将<code>.catch()</code>块链接到那里的末尾。</p>
   </li>
  </ol>

  <p>函数体内部的代码是async(异步)和基于promise的,因此实际上整个函数就像一个promise ––方便啊!</p>
 </li>
 <li>
  <p>接下来,我们调用我们的函数三次以开始获取和解码图像和文本的过程,并将每个返回的promises存储在变量中。在以前的代码下面添加以下内容:</p>

  <pre class="brush: js notranslate">let coffee = fetchAndDecode('coffee.jpg', 'blob');
let tea = fetchAndDecode('tea.jpg', 'blob');
let description = fetchAndDecode('description.txt', 'text');</pre>
 </li>
 <li>
  <p>接下来,我们将定义一个<code>Promise.all()</code>块,仅当上面存储的所有三个promise都已成功完成时才运行一些代码。首先,在<code>.then()</code>调用中添加一个带有空执行程序的块,如下所示:</p>

  <pre class="brush: js notranslate">Promise.all([coffee, tea, description]).then(values =&gt; {

});</pre>

  <p>你可以看到它需要一个包含promises作为参数的数组。执行者只有在所有三个promises的状态成为<strong>resolved</strong>时才会运行;当发生这种情况时,它将被传入一个数组,其中包含来自各个promise(即解码的响应主体)的结果,类似于 [coffee-results, tea-results, description-results].</p>
 </li>
 <li>
  <p>最后,在执行程序中添加以下内容。这里我们使用一些相当简单的同步代码将结果存储在单独的变量中(从blob创建对象URL),然后在页面上显示图像和文本。</p>

  <pre class="brush: js notranslate">console.log(values);
// Store each value returned from the promises in separate variables; create object URLs from the blobs
let objectURL1 = URL.createObjectURL(values[0]);
let objectURL2 = URL.createObjectURL(values[1]);
let descText = values[2];

// Display the images in &lt;img&gt; elements
let image1 = document.createElement('img');
let image2 = document.createElement('img');
image1.src = objectURL1;
image2.src = objectURL2;
document.body.appendChild(image1);
document.body.appendChild(image2);

// Display the text in a paragraph
let para = document.createElement('p');
para.textContent = descText;
document.body.appendChild(para);</pre>
 </li>
 <li>
  <p>保存并刷新,你应该看到所有UI组件都已加载,尽管不是特别有吸引力!</p>
 </li>
</ol>

<p>我们在这里提供的用于显示项目的代码相当简陋,但现在作为解释器。</p>

<div class="blockIndicator note">
<p><strong>注意</strong>: 如果你遇到困难,可以将你的代码版本与我们的代码进行比较,看看它的外观 -––<a href="https://mdn.github.io/learning-area/javascript/asynchronous/promises/promise-all.html">see it live</a>,也可以参阅<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/promise-all.html">source code</a></p>
</div>

<div class="blockIndicator note">
<p><strong>注意</strong>: 如果你正在改进这段代码,你可能想要遍历一个项目列表来显示,获取和解码每个项目,然后循环遍历<code>Promise.all()</code>内部的结果,运行一个不同的函数来显示每个项目取决于什么代码的类型是。这将使它适用于任何数量的项目,而不仅仅是三个。</p>

<p>此外,你可以确定要获取的文件类型,而无需显式类型属性。例如,你可以使用<code><a href="/en-US/docs/Web/API/Headers/get">response.headers.get("content-type")</a></code>检查响应的{{HTTPHeader("Content-Type")}} HTTP标头,然后做出相应的反应。</p>
</div>

<h2 id="在promise_fullfillreject后运行一些最终代码"> 在promise fullfill/reject后运行一些最终代码</h2>

<p>在promise完成后,你可能希望运行最后一段代码,无论它是否已实现(fullfilled)或被拒绝(rejected)。此前,你必须在<code>.then()</code><code>.catch()</code>回调中包含相同的代码,例如:</p>

<pre class="brush: js notranslate">myPromise
.then(response =&gt; {
  doSomething(response);
  runFinalCode();
})
.catch(e =&gt; {
  returnError(e);
  runFinalCode();
});</pre>

<p>在现代浏览器中,<code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally">.finally()</a></code> 方法可用,它可以链接到常规promise链的末尾,允许你减少代码重复并更优雅地执行操作。上面的代码现在可以写成如下:</p>

<pre class="brush: js notranslate">myPromise
.then(response =&gt; {
  doSomething(response);
})
.catch(e =&gt; {
  returnError(e);
})
.finally(() =&gt; {
  runFinalCode();
});</pre>

<p>有关一个真实示例,请查看我们的<a href="https://mdn.github.io/learning-area/javascript/asynchronous/promises/promise-finally.html">promise-finally.html demo</a>(另请参阅<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/promise-finally.html">source code</a>)。这与我们在上面部分中看到的<code>Promise.all()</code>演示完全相同,除了在<code>fetchAndDecode()</code>函数中我们将<code>finally()</code>调用链接到链的末尾:</p>

<pre class="brush: js notranslate"><code>function fetchAndDecode(url, type) {
  return fetch(url).then(response =&gt; {
    if(!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    } else {
      if(type === 'blob') {
        return response.blob();
      } else if(type === 'text') {
        return response.text();
      }
    }
  })
  .catch(e =&gt; {
    console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
  })
  .finally(() =&gt; {
    console.log(`fetch attempt for "${url}" finished.`);
  });
}</code></pre>

<p>这会将一条简单的消息记录到控制台,告诉我们每次获取尝试的时间。</p>

<div class="blockIndicator note">
<p><strong>注意</strong>:finally()允许你在异步代码中编写异步等价物try/ catch / finally。</p>
</div>

<h2 id="构建自定义promise"> 构建自定义promise</h2>

<p>好消息是,在某种程度上,你已经建立了自己的promise。当你使用<code>.then()</code>块链接多个promise时,或者将它们组合起来创建自定义函数时,你已经在创建自己的基于异步声明的自定义函数。例如,从前面的示例中获取我们的<code>fetchAndDecode()</code>函数。</p>

<p>将不同的基于promise的API组合在一起以创建自定义函数是迄今为止你使用promises进行自定义事务的最常见方式,并展示了基于相同原则的大多数现代API的灵活性和强大功能。然而,还有另一种方式。</p>

<h3 id="使用Promise构造函数">使用Promise()构造函数</h3>

<p>可以使用<code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise()</a></code> 构造函数构建自己的promise。当你需要使用现有的旧项目代码、库或框架以及基于现代promise的代码时,这会派上用场。比如,当你遇到没有使用promise的旧式异步API的代码时,你可以用promise来重构这段异步代码。</p>

<p>让我们看一个简单的示例来帮助你入门 —— 这里我们用 promise 包装一了个<code><a href="/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout">setTimeout()</a></code>它会在两秒后运行一个函数,该函数将用字符串“Success!”,解析当前promise(调用链接的<code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve">resolve()</a></code>)。</p>

<pre class="brush: js notranslate">let timeoutPromise = new Promise((resolve, reject) =&gt; {
  setTimeout(function(){
    resolve('Success!');
  }, 2000);
});</pre>

<p><code>resolve()</code><code>reject()</code>是用来<strong>实现</strong><strong>拒绝</strong>新创建的promise的函数。此处,promise 成功运行通过显示字符串“Success!”。</p>

<p>因此,当你调用此promise时,可以将<code>.then()</code>块链接到它的末尾,它将传递给<code>.then()</code>块一串“Success!”。在下面的代码中,我们显示出该消息:</p>

<pre class="brush: js notranslate">timeoutPromise
.then((message) =&gt; {
   alert(message);
})</pre>

<p>更简化的版本:</p>

<pre class="brush: js notranslate">timeoutPromise.then(alert);
</pre>

<p>尝试 <a href="https://mdn.github.io/learning-area/javascript/asynchronous/promises/custom-promise.html">running this live</a> 以查看结果 (可参考 <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/custom-promise.html">source code</a>).</p>

<p>上面的例子不是很灵活 - promise只能<strong>实现</strong>一个字符串,并且它没有指定任何类型的<code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject">reject()</a></code>条件(诚然,<code>setTimeout()</code>实际上没有失败条件,所以对这个简单的例子并不重要)。</p>

<div class="blockIndicator note">
<p><strong>注意</strong>: 为什么要<code>resolve()</code>,而不是<code>fullfill()</code>?我们现在给你的答案有些复杂。</p>
</div>

<h3 id="拒绝一个自定义promise">拒绝一个自定义promise</h3>

<p>我们可以创建一个<code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject">reject()</a></code> 方法拒绝promise  - 就像<code>resolve()</code>一样,这需要一个值,但在这种情况下,它是拒绝的原因,即将传递给<code>.catch()</code>的错误块。</p>

<p>让我们扩展前面的例子,使其具有一些<code>reject()</code>条件,并允许在成功时传递不同的消息。</p>

<p>获取<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/custom-promise.html">previous example</a>副本,并将现有的<code>timeoutPromise()</code>定义替换为:</p>

<pre class="brush: js notranslate">function timeoutPromise(message, interval) {
  return new Promise((resolve, reject) =&gt; {
    if (message === '' || typeof message !== 'string') {
      reject('Message is empty or not a string');
    } else if (interval &lt; 0 || typeof interval !== 'number') {
      reject('Interval is negative or not a number');
    } else {
      setTimeout(function(){
        resolve(message);
      }, interval);
    }
  });
};</pre>

<p>在这里,我们将两个方法传递给一个自定义函数 - 一个用来做某事的消息,以及在做这件事之前要经过的时间间隔。在函数内部,我们返回一个新的<code>Promise</code>对象 - 调用该函数将返回我们想要使用的promise。</p>

<p><code>Promise</code>构造函数中,我们在if ... else结构中进行了一些检查:</p>

<ol>
 <li>首先,我们检查消息是否适合被警告。如果它是一个空字符串或根本不是字符串,我们会使用合适的错误消息拒绝该promise。</li>
 <li>接下来,我们检查间隔是否是适当的间隔值。如果是负数或不是数字,我们会使用合适的错误消息拒绝promise。</li>
 <li>最后,如果参数看起来都正常,我们使用<code>setTimeout()</code>在指定的时间间隔过后,使用指定的消息解析promise。</li>
</ol>

<p>由于<code>timeoutPromise()</code>函数返回一个<code>Promise</code>,我们可以将<code>.then()</code><code>.catch()</code>等链接到它上面以利用它的功能。现在让我们使用它 - 将以前的timeoutPromise用法替换为以下值:</p>

<pre class="brush: js notranslate">timeoutPromise('Hello there!', 1000)
.then(message =&gt; {
   alert(message);
})
.catch(e =&gt; {
  console.log('Error: ' + e);
});</pre>

<p>当你按原样保存并运行代码时,一秒钟后你将收到消息提醒。现在尝试将消息设置为空字符串或将间隔设置为负数,例如,你将能够通过相应的错误消息查看被拒绝的promise!你还可以尝试使用已解决的消息执行其他操作,而不仅仅是提醒它。</p>

<div class="blockIndicator note">
<p><strong>注意</strong>: 你可以在GitHub上找到我们的这个示例版本<a href="https://mdn.github.io/learning-area/javascript/asynchronous/promises/custom-promise2.html">custom-promise2.html</a>(另请参阅<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/promises/custom-promise2.html">source code</a>)。</p>
</div>

<h3 id="一个更真实的例子">一个更真实的例子</h3>

<p>上面的例子是故意做得简单,以使概念易于理解,但它并不是实际上完全异步。异步性质基本上是使用<code>setTimeout()</code>伪造的,尽管它仍然表明promises对于创建具有合理的操作流程,良好的错误处理等的自定义函数很有用</p>

<p>我们想邀请你学习的一个例子是<a href="https://github.com/jakearchibald/idb/">Jake Archibald's idb library</a>,它真正地显示了<code>Promise()</code>构造函数的有用异步应用程序。这采用了 <a href="/en-US/docs/Web/API/IndexedDB_API">IndexedDB API</a>,它是一种旧式的基于回调的API,用于在客户端存储和检索数据,并允许你将其与promises一起使用。如果你查看<a href="https://github.com/jakearchibald/idb/blob/master/lib/idb.js">main library file</a>,你将看到我们在上面讨论过的相同类型的技术。以下块将许多IndexedDB方法使用的基本请求模型转换为使用promise:</p>

<pre class="brush: js notranslate">function promisifyRequest(request) {
  return new Promise(function(resolve, reject) {
    request.onsuccess = function() {
      resolve(request.result);
    };

    request.onerror = function() {
      reject(request.error);
    };
  });
}</pre>

<p>这可以通过添加一些事件处理程序来实现,这些事件处理程序在适当的时候实现(fullfill)和拒绝(reject)promise。</p>

<ul>
 <li><code><a href="/en-US/docs/Web/API/IDBRequest">request</a></code><a href="/en-US/docs/Web/API/IDBRequest/success_event"><code>success</code> event</a>触发时,<code><a href="/en-US/docs/Web/API/IDBRequest/onsuccess">onsuccess</a></code>处理程序将使用请求的<code><a href="/en-US/docs/Web/API/IDBRequest/result">result</a></code>实现(fullfill)promise。</li>
 <li><code><a href="/en-US/docs/Web/API/IDBRequest">request</a></code><a href="/en-US/docs/Web/API/IDBRequest/error_event"><code>error</code> event</a>触发时,<code><a href="/en-US/docs/Web/API/IDBRequest/onerror">onerror</a></code>处理程序拒绝带有请求<code><a href="/en-US/docs/Web/API/IDBRequest/error">error</a></code>的promise</li>
</ul>

<h2 id="总结">总结</h2>

<p>当我们不知道函数的返回值或返回需要多长时间时,Promises是构建异步应用程序的好方法。它们使得在没有深度嵌套回调的情况下更容易表达和推理异步操作序列,并且它们支持类似于同步<code>try ... catch</code>语句的错误处理方式。</p>

<p>Promise适用于所有现代浏览器的最新版本;promise有兼容问题的唯一情况是Opera Mini和IE11及更早版本。</p>

<p>本文中,我们没有涉及的所有promise的功能,只是最有趣和最有用的功能。当你开始了解有关promise的更多信息时,你会遇到更多功能和技巧。</p>

<p>大多数现代Web API都是基于promise的,因此你需要了解promise才能充分利用它们。这些API包括<a href="/en-US/docs/Web/API/WebRTC_API">WebRTC</a><a href="/en-US/docs/Web/API/Web_Audio_API">Web Audio API</a><a href="/en-US/docs/Web/API/Media_Streams_API">Media Capture and Streams</a>等等。随着时间的推移,Promises将变得越来越重要,因此学习使用和理解它们是学习现代JavaScript的重要一步。</p>

<p><font face="x-locale-heading-primary, zillaslab, Palatino, Palatino Linotype, x-locale-heading-secondary, serif"><span style="font-size: 37.33327865600586px;"><strong>参见</strong></span></font></p>

<ul>
 <li><code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise()</a></code></li>
 <li><a href="/en-US/docs/Web/JavaScript/Guide/Using_promises">Using promises</a></li>
 <li><a href="https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html">We have a problem with promises</a> by Nolan Lawson</li>
</ul>

<p>{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Timeouts_and_intervals", "Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}</p>

<h2 id="本章内容"> 本章内容</h2>

<ul>
 <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Concepts">异步编程的基本概念</a></li>
 <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Introducing">JavaScript异步编程简介</a></li>
 <li><a href="/zh-CNdocs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">合作的异步JavaScript:超时和间隔</a></li>
 <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Promises">使用Promises实现优雅的异步编程</a></li>
 <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Async_await">使用async和await让异步编程更简单</a></li>
 <li><a href="/zh-CN/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach">选择正确的方法</a></li>
</ul>