JavaScript Promise:简介

Promise 可简化延迟和异步计算。promise 表示尚未完成的操作。

Jake Archibald
Jake Archibald

开发者们,请做好准备,迎接 Web 开发史上的关键时刻。

[鼓点响起]

Promise 已获得 JavaScript 的原生支持!

[烟花绽放、彩纸飘飘、人群沸腾]

此时,您属于以下某个类别:

  • 人群在您身边欢呼雀跃,但是您感到莫名其妙。可能您甚至连“promise”是什么都不知道。因此您耸耸肩,但是从天而降的彩纸虽轻如鸿毛却让您无法释怀。如果真是这样,您也无需担心,我可花了很长的时间才弄明白为什么我应该关注它。您可能想从开头开始。
  • 您非常抓狂!觉得晚了一步,对吗?您可能之前使用过这些 Promise,但让您困扰的是,不同版本的 API 各有差异。官方 JavaScript 版本的 API 是什么?您可能想要从术语开始。
  • 您已知道这些,您会觉得那些上窜下跳的人很好笑,居然把它当作新闻。您可以先自豪一把,然后直接查看 API 参考文档

浏览器支持和 polyfill

浏览器支持

  • Chrome:32.
  • Edge:12.
  • Firefox:29.
  • Safari:8.

来源

如要使没有完全实现 promise 的浏览器符合规范,或向其他浏览器和 Node.js 中添加 promise,请查看此 polyfill(gzip 压缩大小为 2k)。

人们究竟为何欢呼雀跃?

JavaScript 是单线程工作,这意味着两段脚本不能同时运行,而是必须一个接一个地运行。在浏览器中,JavaScript 与因浏览器而异的其他 N 种任务共享一个线程。但通常情况下,JavaScript 与绘制、更新样式和处理用户操作(例如,突出显示文本以及与表单控件互动)位于同一队列中。操作其中一项任务会延迟其他任务。

我们人类是多线程工作。您可以使用多根手指打字 可以一边开车一边与人交谈我们唯一必须处理的阻止功能是打喷嚏。在打喷嚏期间,所有当前活动都必须暂停。这真是非常讨厌,尤其是当您在开车并想与人交谈时。您可不想编写像打喷嚏似的代码。

您可能已使用事件和回调来解决该问题。以下是一些事件:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

这可不会像打喷嚏那样打断您。我们获得图片、添加几个监听器,之后 JavaScript 可停止执行,直至其中一个监听器被调用。

遗憾的是,在上例中,事件有可能在我们开始监听之前就发生了,因此我们需要使用图像的“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
});

这不会捕获出错的图像,因为在此之前我们没有机会侦听到错误。遗憾的是,DOM 也没有给出解决之道。此外,这只会加载一张图片。如果我们希望知道一组图像的加载时间,情况会变得更加复杂。

事件并不总是最佳方法

事件非常适合在同一对象上发生多次的事件(keyuptouchstart 等)。对于这些事件,您并不真正关心在附加监听器之前发生了什么。但是,如果关系到异步成功/失败,理想的情况是您希望:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

这是 promise 所执行的任务,但以更好的方式命名。如果 HTML 图像元素有一个返回 promise 的“ready”方法,我们可以执行:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

最基本的情况是,promise 有点类似于事件监听器,但有以下两点区别:

  • promise 只能成功或失败一次。而不能成功或失败两次,也不能从成功转为失败或从失败转为成功。
  • 如果 promise 已成功或失败,且您之后添加了成功/失败回调,则系统会调用正确的回调,即使事件发生在先。

这对于异步成功/失败非常有用,因为您对某项内容可用的确切时间不太关注,而更关注于对结果做出响应。

Promise 术语

Domenic Denicola 校对了本篇文章的初稿,并在术语方面给我打分为“F”。他把我拘留,强迫我抄写状态和命运 100 遍,并给我的父母写了封信。尽管如此,我还是对很多术语混淆不清,以下是几个基本的概念:

promise 可以是:

  • 已执行 - 与 promise 有关的操作成功
  • 已拒绝 - 与 promise 有关的操作失败
  • 待处理 - 尚未执行或拒绝
  • 已解决 - 已执行或拒绝

规范还使用术语 thenable 来描述类似于 promise 的对象,并使用 then 方法。该术语让我想起前英格兰国家队教练 Terry Venables,因此我将尽可能不用这个术语。

Promise 在 JavaScript 中受支持!

Promise 有一段时间以库的形式出现,例如:

以上这些与 JavaScript promise 都有一个名为 Promises/A+ 的常见标准化行为。如果您是 jQuery 用户,他们还有一个类似于名为 Deferreds 的行为。但是,Deferred 与 Promise/A+ 不兼容,这使得它们存在细微差异且不太实用,因此请注意。jQuery 也有一个 Promise 类型,但这只是 Deferred 的一个子集,存在相同的问题。

尽管 promise 实现遵照标准化行为,但其整体 API 有所不同。JavaScript promise 在 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"));
  }
});

promise 构造函数采用一个参数,以及一个包含两个参数(resolve 和拒绝)的回调。在回调中执行一些操作(例如异步),如果一切都正常,则调用 resolve,否则调用 reject。

与普通旧版 JavaScript 中的 throw 一样,通常拒绝时会给出 Error 对象,但这不是必须的。Error 对象的优点在于它们能够捕获堆栈轨迹,因而使得调试工具非常有用。

以下是有关 promise 的使用示例:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() 包含两个参数:一个用于成功情形的回调和一个用于失败情形的回调。这两个都是可选的,因此您可以只添加一个用于成功情形或失败情形的回调。

JavaScript promise 最初是在 DOM 中出现并称为“Futures”,之后重命名为“Promises”,最后又移入 JavaScript。在 JavaScript 中使用比在 DOM 中更好,因为它们将在如 Node.js 等非浏览器 JS 环境中可用(而它们是否会在核心 API 中使用 Promise 则是另外一个问题)。

尽管它们是 JavaScript 的一项功能,但 DOM 也能使用。实际上,采用异步成功/失败方法的所有新 DOM API 均使用 promise。配额管理字体加载事件ServiceWorkerWeb MIDIStreams 等都已做到这一点。

与其他库的兼容性

JavaScript promise API 会将任何使用 then() 方法视为类似于 promise 的方法(或者 thenable,按照 promise 的说法应当是吧),因此,如果您使用返回 Q promise 的库,也没关系,它可以很好地与新的 JavaScript 结合使用。

不过,正如我之前提到的,jQuery 的 Deferred 有点...没有用。幸运的是,您可以将其转为标准 promise,这值得尽快去做:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

这里,jQuery 的 $.ajax 返回一个 Deferred。由于它使用 then() 方法,因此 Promise.resolve() 可将其转为 JavaScript promise。但是,有时 deferred 会将多个参数传递给其回调,例如:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

而 JS promise 会忽略除第一个之外的所有参数:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

幸好,通常这就是您想要的,或者至少为您提供了方法让您获得所想要的。另请注意,jQuery 不遵循将 Error 对象传递到 reject 这一惯例。

复杂异步代码让一切变得更简单

接下来,让我们写一些代码。假设我们想要:

  1. 启动一个旋转图标以指示正在加载
  2. 获取一个故事的 JSON,确定每个章节的标题和网址
  3. 为页面添加标题
  4. 获取每个章节
  5. 将故事添加到页面
  6. 停止旋转图标

…但如果此过程发生错误,也要向用户显示。我们也想在那时停止旋转图标,否则它将不断旋转、眩晕并撞上其他界面。

当然,您不会使用 JavaScript 来提供故事,以 HTML 形式提供会更快,但是这种方式在处理 API 时很常见:多次提取数据,然后在所有数据提取完毕后执行某项操作。

首先,让我们从网络中获取数据:

对 XMLHttpRequest 执行 promise

旧 API 将更新为使用 promise,如有可能,采用后向兼容的方式。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();
  });
}

现在,让我们来使用它:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

现在,我们无需手动输入 XMLHttpRequest 即可发出 HTTP 请求,这真是太棒了,因为越少看到令人讨厌的 XMLHttpRequest 驼峰式大小写,我的生活就越快乐。

then() 不是最终部分,您可以将各个 then 链接在一起来改变值,或依次运行额外的异步操作。

转换值

只需返回新值即可改变值:

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
})

举一个实际的例子,让我们回到:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

这里的 response 是 JSON,但是我们当前收到的是其纯文本。我们可以将 get 函数修改为使用 JSON responseType,不过我们也可以使用 promise 来解决这个问题:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

由于 JSON.parse() 接受单个参数并返回转换后的值,因此我们可以将其简化为:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

实际上,我们可以非常轻松地构建 getJSON() 函数:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() 仍会返回一个 promise,该 promise 获取网址,然后将响应解析为 JSON。

异步操作队列

您还可以链接多个 then,以便按顺序运行异步操作。

当您从 then() 回调中返回某些内容时,这有点儿神奇。如果返回一个值,则会以该值调用下一个 then()。但是,如果您返回类似于 promise 的内容,下一个 then() 则会等待,并仅在 promise 产生结果(成功/失败)时调用。例如:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

这里我们向 story.json 发出异步请求,这可让我们请求一组网址,随后我们请求其中的第一个。这是 promise 从简单回调模式中脱颖而出的真正原因所在。

您甚至可以采用更简短的方法来获得章节内容:

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);
})

直到 getChapter 被调用,我们才下载 story.json,但是下次 getChapter 被调用时,我们重复使用 story promise,因此 story.json 仅获取一次。这就是 Promise 的魅力所在!

错误处理

正如我们之前所看到的,then() 包含两个参数:一个用于成功,一个用于失败(按照 promise 中的说法,即执行和拒绝):

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

您还可以使用 catch()

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch() 没有什么特别之处,它只是 then(undefined, func) 的锦上添花,但可读性更强。请注意,以上两个代码示例行为并不相同,后者相当于:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

两者之间的差异虽然很微小,但非常有用。Promise 拒绝后,将跳至带有拒绝回调的下一个 then()(或具有相同功能的 catch())。如果是 then(func1, func2),则 func1func2 中的一个将被调用,而不会二者均被调用。但使用 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!");
})

以上流程与常规的 JavaScript try/catch 非常类似,在“try”中发生的错误直接进入 catch() 块。以下是上述流程图的流程图(因为我喜欢流程图):

蓝线表示执行的 promise 路径,红路表示拒绝的 promise 路径。

JavaScript 异常和 promise

当 promise 被明确拒绝时,会发生拒绝;但如果构造函数回调中抛出错误,也会隐式拒绝:

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);
})

这意味着,在 promise 构造函数回调内部执行所有与 promise 相关的任务很有用,因为错误会自动捕获并进而拒绝。

对于在 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);
})

错误处理实践

在我们的故事和章节中,我们可使用 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';
})

如果提取 story.chapterUrls[0] 失败(例如,http 500 或用户处于离线状态),它将跳过所有后续成功回调,包括 getJSON() 中尝试将响应解析为 JSON 的回调,还会跳过将 chapter1.html 添加到页面的回调。而是会移至 catch 回调。因此,如果前面的任何操作失败,“Failed to show chapter”将会添加到页面。

与 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'

您可能想出于记录目的而 catch(),而无需从错误中恢复。为此,只需重新抛出错误即可。我们可以在 getJSON() 方法中执行此操作:

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

至此,我们已获取其中一个章节,但我们想要所有的章节。我们来实现这一点。

并行式和顺序式:两者兼得

异步并不容易。如果您觉得难以着手,可尝试按照同步的方式编写代码。在此示例中:

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'

这样就行了!但这是同步的情况,而且在内容下载时浏览器会被锁定。为了使其异步,我们使用 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';
})

但是我们如何遍历章节的网址并按顺序获取呢?不起作用

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach 并非异步的,因此我们的章节内容将按照下载的顺序显示,这就乱套了。我们不是在写非线性叙事小说,因此得解决该问题。

创建序列

我们想要将 chapterUrls 数组转变为 promise 序列。我们可以使用 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);
  });
})

这是我们第一次看到 Promise.resolve(),它创建的 promise 可解析为您赋予的任何值。如果向其传递一个 Promise 实例,它也会将其返回(注意:这是对本规范的一处更改,某些实现尚未遵循)。如果将类似于 promise 的内容(带有 then() 方法)传递给它,它将创建以相同方式执行/拒绝的真正 Promise。如果您传入任何其他值,例如Promise.resolve('Hello'),它会创建一个使用该值执行的 Promise。如果调用时不带任何值(如上所示),它在执行时将返回“undefined”。

此外还有 Promise.reject(val),它创建的 promise 在拒绝时将返回赋予的值(或“undefined”)。

我们可以使用 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())

这与之前示例的做法相同,但是不需要独立的“sequence”变量。系统会针对数组中的每个项调用缩减回调。 首次调用时,“sequence”为 Promise.resolve(),但是对于余下的调用,“sequence”为我们从之前调用中返回的值。array.reduce 确实非常有用,它将数组浓缩为一个简单的值(在本例中,该值为 promise)。

我们来总结一下:

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';
})

现在我们已经实现,即同步版本的完全异步版本。但我们可以做得更好。此时,我们的页面正在下载,如下所示:

浏览器的一个优势在于可以一次下载多个内容,因此我们一章章地下载就失去了其优势。我们希望同时下载所有章节,然后在所有下载完毕后进行处理。幸运的是,API 可帮助我们实现:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all 包含一组 promise,并创建一个在所有内容成功完成后执行的 promise。您将获得一组结果(即 promise 执行的任意内容),其顺序与您传入 promise 的顺序相同。

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';
})

根据连接情况,这可能比一个个依次加载要快几秒钟,而且代码也比我们第一次尝试的要少。章节可按任意顺序下载,但在屏幕中以正确顺序显示。

不过,我们仍然可以提升感知性能。第一章下载完后,我们可将其添加到页面。这可让用户在其他章节下载完毕前先开始阅读。第三章下载完后,我们不将其添加到页面,因为用户可能没意识到还缺少第二章。第二章下载完后,我们可添加第二章和第三章,以此类推。

为此,我们使用 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';
})

我们做到了,两全其美!传送所有内容所用的时间相同,但用户可先阅读前面的内容。

在这个小示例中,所有章节几乎同时下载完毕,但是如果一本书有更多、更长的章节,一次显示一个章节的优势便会更明显。

使用 Node.js 风格的回调或事件来执行以上示例需两倍代码,更重要的是,没那么容易实施。然而,promise 功能还不止如此,与其他 ES6 功能组合使用时,它们甚至更容易。

额外奖励:扩展功能

自我最初撰写本文以来,Promise 的使用能力已大大扩展。从 Chrome 55 开始,异步函数允许像编写同步代码那样编写基于 Promise 的代码,而且还不会阻塞主线程。如需了解详情,请参阅我的异步函数文章。主要浏览器广泛支持 Promise 和异步函数。如需了解详情,请参阅 MDN 的 Promise异步函数参考文档。

Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、Addy Osmani、Arthur Evans 和 Yutaka Hirano 对本篇文章进行了校对,提出了建议并作出了修正,特此感谢!

此外,Mathias Bynens 负责本篇文章的更新部分,特此致谢。