原文链接:http://javascript.info/promise-basics,translate with ❤️ by zhangbao.

想象一下,你是一名顶尖歌手,粉丝们会日夜问你即将到来的单曲。

为了得到一些缓解,你保证在发布时将它发送给他们。您可以为粉丝提供可以订阅的更新列表,在填写他们电子邮件地址后,就可以在歌曲发布时,收到通知;即使出现问题,例如,如果发布歌曲的计划被取消,仍会收到通知。

每个人都很开心,因为人们不再拥挤你和粉丝也得到满足——因为不会错过单曲。

这是我们在编程中经常遇到的一个类比:

  1. “生产代码”,做一些花费时间的事情。例如,加载一个远程脚本。对比到上例中的“歌手”。

  2. “消费代码”,依赖“生成代码”产生的结果。许多功能的实现可能需要这个结果。对应上例中的“粉丝”。

  3. Promise 是一个特殊的 JavaScript 对象,它将“生成代码”和“消费代码”链接在一起。 就我们的比喻而言:这就是“订阅列表”。“生成代码”需要花费一些时间来产生 Promise 的结果,一旦结果准备好,所有的订阅代码都能获得执行。

这个类比并不十分准确,因为 JavaScript 的 Promise 比的订阅列表更复杂:它们有附加的特性和限制。但一开始从它展开理解会很好。

Promise 构造函数语法是:

  1. let promise = new Promise(function(resolve, reject) {
  2. // 执行器 (生产代码, "歌手")
  3. });

传递给 new Promise() 构造函数的回调称为执行器。创建 Promise 的时候,执行器函数会自动执行,它包含生产代码,最终产生一个结果。类比到上例中:执行器就是“歌手”。

生成的 promise 对象具有内部属性:

  • state——初始状态是“pending”,之后改变为“fulfilled”或“rejected”,

  • result——你选择的任意值,初始值为 undefined

当执行器执行完毕后,它应该调用传递进去的两个参数(函数类型的)之一:。

  • resolve(value)——表示任务成功完成:

    • state 值设置为“fulfilled”,

    • result 值设置为 value

  • reject(error)——表示错误发生:

    • state 值设置为“rejected”,

    • result 值设置为 error

Promise - 图1

稍后我们将看到这些变化是如何通知到“粉丝”的。

下面是一个 Promise 构造函数和一个具有简单“生成代码”的执行器例子(使用了 setTimeout):

  1. let promise = new Promise(function(resolve, reject) {
  2. // 调用 Promise 构造函数的时候,这个函数会自动执行
  3. // 一秒后使用结果 "done!" 结束任务
  4. setTimeout(() => resolve("done!"), 1000);
  5. });

运行上面的代码,我们可以看到:

  1. 执行器会立即自动调用(通过 new Promise)。

  2. 执行器接受两个参数:resolvereject——这些函数是由 JavaScript 引擎预定义的。因此我们无需创建它们。相反,在执行器准备好时,就能直接调用它们了。

经过 1 秒钟的“处理”之后,执行器调用 resolve('done') 产生结果:

Promise - 图2

这是一个成功结束的例子,得到了一个“fulfilled 状态的 Promise”。

现在,在举一个使用错误 reject Promise 的例子:

  1. let promise = new Promise(function(resolve, reject) {
  2. // 1秒钟后任务携带错误结束
  3. setTimeout(() => reject(new Error("Whoops!")), 1000);
  4. });

Promise - 图3

总而言之,执行器里会先做一件事情(通常需要花费点时间),之后调用 resolvereject 来改变 Promise 对象状态。

对应“pending”状态的 Promise,resolved 或 rejected 状态的 Promise 称为是“解决”了的。

Promise - 图4结果只有一个:成功或者失败

在执行器中,仅能调用 resolve 或者 reject,Promise 状态一旦改变,就无法再更改。

再有调用 resolve/reject 的地方都会被忽略:

```javascript let promise = new Promise(function(resolve, reject) { resolve(“done”);

reject(new Error(“…”)); // 忽略 setTimeout(() => resolve(“…”)); // 忽略 });

  1. >
  2. > 其思想是,执行器只能产生一种结果:正常返回或发生错误。
  3. >
  4. > 而且,`resolve`/`reject` 只接受一个参数的调用,其余参数都会被忽略。
  5. > ![](https://cdn.nlark.com/yuque/0/2018/png/103346/1544062755364-fcf09fa7-0c35-481e-8116-c0679055a0c9.png#width=27)**使用 Error 对象 reject**
  6. >
  7. > 一旦发生错误,我们可以使用任何类型参数调用 `reject`(就像 `resolve`)。但是建议使用 `Error` 对象(或者继承自 `Error` 的其他类型),这样做的理由很快就会显现出来。
  8. > ![](https://cdn.nlark.com/yuque/0/2018/png/103346/1544062755364-fcf09fa7-0c35-481e-8116-c0679055a0c9.png#width=27)**立即调用 `resolve`/`reject`**
  9. >
  10. > 实践中,执行器中通常会做一些异步操作,经过一段时间之后才调用 `resolve`/`reject`,但这不是必须的。我们也可以立即调用 `resolve`/`reject`
  11. >
  12. > ```javascript
  13. let promise = new Promise(function(resolve, reject) {
  14. // 没有费时操作,直接结果任务
  15. resolve(123); // 立即给出结果: 123
  16. });

例如,当我们开始做一些操作时,发现一切都已完成时,就对应这种情况。

这很好。我们马上就得到了一个 resolved 状态的 Promise,并且没有错。

Promise - 图5stateresult 都是内部属性 Promise - 图6

Promise 对象上的 stateresult 属性都是内部属性。我们不能直接在“消费代码”里直接访问。我们可以使在 .then/.catch 方法中使用它们产生的结果。它们会在下面介绍。

消费者:“then”和“catch”

Promise 对象作为执行器(“生产代码”或叫“歌手”)和消费代码(“粉丝”)之间的连接,用来接收结果或错误。消费函数是通过 .then.catch 方法注册(订阅)的。

.then 的语法是这样的:

  1. promise.then(
  2. function(result) { /* 处理成功结果 */ },
  3. function(error) { /* 处理错误 */ }
  4. );

.then 的第一个参数是一个函数:

  1. 在 Promise 变为 resolved 状态时执行,并且

  2. 接受处理结果作为参数。

第二个参数也是一个函数:

  1. 在 Promise 变为 rejected 状态时执行,并且

  2. 接受错误作为参数。

下面是处理成功返回的 Promise 的例子:

  1. let promise = new Promise(function(resolve, reject) {
  2. setTimeout(() => resolve("done!"), 1000);
  3. });
  4. // Promise 变为 resolved 状态,执行 .then 的第一个(函数类型)参数
  5. promise.then(
  6. result => alert(result), // 1秒后,显示 "done!"
  7. error => alert(error) // 不会执行
  8. );

第一个函数被执行。

如果 Promise reject 了,则执行第二个:

  1. let promise = new Promise(function(resolve, reject) {
  2. setTimeout(() => reject(new Error("Whoops!")), 1000);
  3. });
  4. // Promise 变为 rejectd 状态,执行 .then 方法的第二个参数
  5. promise.then(
  6. result => alert(result), // 不会执行
  7. error => alert(error) // 1秒后,显示 "Error: Whoops!"
  8. );

如果我们只关心成功结束的情况,那么我们可以为 .then 只提供一个参数。

  1. let promise = new Promise(resolve => {
  2. setTimeout(() => resolve("done!"), 1000);
  3. });
  4. promise.then(alert); // 1秒后,显示 "done!"

如果我们只关闭错误情况,那么可以使用 null 作为 .then 方法的第一个参数:.then(null, errorHandlingFunction),这与使用 .catch(errorHandlingFunction) 是一样的:

  1. let promise = new Promise((resolve, reject) => {
  2. setTimeout(() => reject(new Error("Whoops!")), 1000);
  3. });
  4. // .catch(f) 等同于 promise.then(null, f)
  5. promise.catch(alert); // 1秒后,显示 "Error: Whoops!"

.catch 完全可以看作是 .then(null, f) 的一种简写形式。

Promise - 图7对于已解决 Promise,.then 方法会立即执行

如果 Promise 处于 pending 状态,那么 .then/catch 方法会一直等待结果出来才执行。如果 Promise 是已解决状态的话,就会立即执行 .then/catch

```javascript // resolved 状态 Promise,会立即执行 .then let promise = new Promise(resolve => resolve(“done!”));

promise.then(alert); // done! (立即显示)

  1. >
  2. > 有些任务的完成,有时需要些时间,有时则会立即完成。无论哪种情形,好处是:`.then` 处理程序都能保证正确的运行。
  3. > ![](https://cdn.nlark.com/yuque/0/2018/png/103346/1544062755364-fcf09fa7-0c35-481e-8116-c0679055a0c9.png#width=27)**`.then`/`catch` 处理器总是异步执行的**
  4. >
  5. > 即使 Promise 立即得到解决,`.then`/`catch` 方法下面的代码也会先执行。
  6. >
  7. > JavaScript 引擎内部维护了一个执行队列,收集所有的 `.then`/`catch` 处理器。
  8. >
  9. > 但是,只有在当前执行队列完成时,它才会查看这个队列。
  10. >
  11. > 也就是说,`.then`/`catch` 处理器直到引擎执行完当前代码后,才开始执行。
  12. >
  13. > 例如,这里:
  14. >
  15. > ```javascript
  16. // "立即" resolved 的 Promise
  17. const executor = resolve => resolve("done!");
  18. const promise = new Promise(executor);
  19. promise.then(alert); // 这个 alert 是后展示 (*)
  20. alert("code finished"); // 这个 alert 先展示

上面的 Promise 立即得到解决,但是引擎首先完成的是当前代码,调用 alert然后才查看 .then 处理器队列,去运行。

因此,.then 之后的代码总是在 Promise 订阅者之前执行。即使是对于一个立即得到解决的 Promise。

这通常并不重要,但在某些情况下,是在意顺序的。

接下来,我们要看更多实际的例子,解释 Promise 如何助力我们编写异步代码的。

例子:loadScript

在上一章里,我们定义了一个 loadScript 函数。

这种写法是基于回调的,在这里为了提醒我们写过的东西:

  1. function loadScript(src, callback) {
  2. let script = document.createElement('script');
  3. script.src = src;
  4. script.onload = () => callback(null, script);
  5. script.onerror = () => callback(new Error(`Script load error ` + src));
  6. document.head.append(script);
  7. }

让我们用 Promise 来重写它。

  1. function loadScript(src) {
  2. return new Promise(function(resolve, reject) {
  3. let script = document.createElement('script');
  4. script.src = src;
  5. script.onload = () => resolve(script);
  6. script.onerror = () => reject(new Error("Script load error: " + src));
  7. document.head.append(script);
  8. });
  9. }

使用:

  1. let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js");
  2. promise.then(
  3. script => alert(`${script.src} is loaded!`),
  4. error => alert(`Error: ${error.message}`)
  5. );
  6. promise.then(script => alert('One more handler to do something else!'));

我们可以立即看到基于回调的模式的一些好处:

短处 长处

- 当调用 loadScript 的时候,必须提供一个 callback 函数。也就是说,我们在调用 loadScript之前就要知道要对结果怎样处理。


- 只能提供一个回调函数。
|
- Promise 允许我们按照自然规律做事。首先,我们执行 loadScript,然后我们在 .then 里写对结果执行的操作。
- 我们可以在一个 Promise 上多次调用 .then 方法。每一次,我们添加一个新的“粉丝”。一个新的订阅函数,到“订阅列表”。下一节将详细介绍:Promise 链式调用。
|

因此,Promise 已经为我们提供更好了的代码流和灵活性。下一章中,我们会看到更多的 Promise 相关知识。

练习题

问题

一、可以重复 resolve 一个 Promise?

下面代码的输出结果是什么?

  1. let promise = new Promise(function(resolve, reject) {
  2. resolve(1);
  3. setTimeout(() => resolve(2), 1000);
  4. });
  5. promise.then(alert);

二、延迟 Promise

内置函数 setTimeout 使用回调。创建一个可选的基于 Promise 的形式。

函数 delay(ms) 返回一个 Promise。Promise 会在 ms 毫秒之后 resolve,因此我们可以在之后加 .then 来处理。像这样:

  1. function delay(ms) {
  2. // 你的代码
  3. }
  4. delay(3000).then(() => alert('3秒后运行'));

三、使用 Promise 实现动圆功能

重写一下 Animated circle with callback 任务的 showCircle 函数实现,改成返回 Promise 而非使用回调的形式。

新的使用方式:

  1. showCircle(150, 150, 100).then(div => {
  2. div.classList.add('message-ball');
  3. div.append("Hello, world!");
  4. });

作为基础,你可以先看下 Animated circle with callback 任务的实现效果。

答案

一、可以重复 resolve 一个 Promise?

输出是 1

第二个 resolve 调用会被忽略,因为只有第一次的 reject/resolve 调用是有效的。其他再多一次的调用都会被忽略。

二、延迟 Promise?

  1. function delay(ms) {
  2. return new Promise(resolve => setTimeout(resolve, ms));
  3. }
  4. delay(3000).then(() => alert('3秒后运行'));

需要注意的是,这里的 resolve 函数在调用时,没有提供参数。delay 没有返回任何值,只是为了延迟时间。

三、使用 Promise 实现动圆功能

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <style>
  6. .message-ball {
  7. font-size: 20px;
  8. line-height: 200px;
  9. text-align: center;
  10. }
  11. .circle {
  12. transition-property: width, height, margin-left, margin-top;
  13. transition-duration: 2s;
  14. position: fixed;
  15. transform: translateX(-50%) translateY(-50%);
  16. background-color: red;
  17. border-radius: 50%;
  18. }
  19. </style>
  20. </head>
  21. <body>
  22. <button onclick="go()">Click me</button>
  23. <script>
  24. function go() {
  25. showCircle(150, 150, 100).then(div => {
  26. div.classList.add('message-ball');
  27. div.append("Hello, world!");
  28. });
  29. }
  30. function showCircle(cx, cy, radius) {
  31. let div = document.createElement('div');
  32. div.style.width = 0;
  33. div.style.height = 0;
  34. div.style.left = cx + 'px';
  35. div.style.top = cy + 'px';
  36. div.className = 'circle';
  37. document.body.append(div);
  38. return new Promise(resolve => {
  39. setTimeout(() => {
  40. div.style.width = radius * 2 + 'px';
  41. div.style.height = radius * 2 + 'px';
  42. div.addEventListener('transitionend', function handler() {
  43. div.removeEventListener('transitionend', handler);
  44. resolve(div);
  45. });
  46. }, 0);
  47. })
  48. }
  49. </script>
  50. </body>
  51. </html>

在线查看

(完)