JavaScript Promise:简介

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

Jake Archibald
Jake Archibald

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

[Drumroll begins]

JavaScript 中推出了 promise!

[烟花绽放,闪亮的纸片从天而降,人群沸腾]

目前,您属于以下某个类别:

  • 周围的人在欢呼,但您不确定发生了什么。也许你甚至不确定“承诺”是什么。你会耸耸肩,但闪亮的纸张的重量压在你的肩膀上。如果是这样,请不要担心,我花了好长时间才弄清楚为什么我应该关心这些事。您可能需要从开始
  • 你向空中挥拳!来得正是时候,对吗?您之前使用过这些 Promise 功能,但所有实现的 API 略有不同,这让您感到困扰。官方 JavaScript 版本的 API 是什么?建议您先了解术语
  • 您早就知道这一点,并嘲笑那些像是刚刚知道这个消息一样兴奋不已的人。不妨花点时间享受一下自己的优越感,然后直接前往 API 参考文档

浏览器支持和 polyfill

Browser Support

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

Source

如需使缺少完整 Promise 实现的浏览器符合规范,或向其他浏览器和 Node.js 添加 Promise,请查看此 polyfill(2KB,经过 GZIP 压缩)。

这到底是怎么回事?

JavaScript 是单线程的,这意味着两段脚本无法同时运行;它们必须依次运行。在浏览器中,JavaScript 会与大量其他内容共享一个线程,这些内容因浏览器而异。但通常,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”的评分。他把我关禁了,强迫我抄写《States and Fates》(州与命运)100 遍,还写了一封担心不安的信给我父母。尽管如此,我仍然会混淆许多术语,但下面列出了一些基本术语:

一个 promise 可以是:

  • 已执行 - 与 promise 相关的操作已成功
  • rejected - 与 promise 相关的操作失败
  • 待处理 - 尚未完成或拒绝
  • settled - 已完成或遭拒

规范还使用术语 thenable 来描述类似于 Promise 的对象,因为它具有 then 方法。这个术语让我想起了前英格兰足球队经理 Terry Venables,因此我会尽量少用它。

JavaScript 中推出了 promise!

承诺已以库的形式存在了一段时间,例如:

上述 Promise 和 JavaScript Promise 具有一种称为 Promises/A+ 的通用标准化行为。如果您是 jQuery 用户,则可以使用类似的 Deferred。不过,延迟操作不符合 Promise/A+ 规范,这使得它们略有不同且用处不大,因此请注意。jQuery 也具有Promise 类型,但这只是延迟操作的一部分,并且存在相同的问题。

虽然 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 和 reject)的回调。在回调中执行某些操作(可能为异步操作),然后如果一切正常,则调用 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 中被称为“Future”,后来重命名为“Promise”,最后移到了 JavaScript 中。将它们放在 JavaScript 中而不是 DOM 中非常有用,因为它们将在 Node.js 等非浏览器 JS 上下文中可用(它们是否会在其核心 API 中使用它们是另一个问题)。

虽然它们是 JavaScript 功能,但 DOM 并不惧怕使用它们。事实上,所有具有异步成功/失败方法的新 DOM API 都将使用 Promise。配额管理字体加载事件ServiceWorkerWeb MIDI等功能已经实现了这一点。

与其他库的兼容性

JavaScript Promises API 会将使用 then() 方法的任何内容视为 Promise 类似内容(或 Promise 术语中的 thenable 叹息),因此,如果您使用的是会返回 Q Promise 的库,则没关系,它可以与新的 JavaScript Promise 很好地配合使用。

不过,正如我之前所提到的,jQuery 的延迟操作有点……没用。幸运的是,您可以将它们转换为标准 promise,并且值得尽快这样做:

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

在这里,jQuery 的 $.ajax 会返回一个 Deferred。由于它具有 then() 方法,因此 Promise.resolve() 可以将其转换为 JavaScript promise。不过,有时延迟调用会向其回调传递多个参数,例如:

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

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

而 JS 承诺会忽略除第一个之外的所有其他承诺:

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

幸运的是,这通常就是您想要的,或者至少能让您访问所需内容。另请注意,jQuery 不遵循将 Error 对象传递到拒绝项的惯例。

简化复杂的异步代码

好的,我们来编写一些代码。假设我们要:

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

响应是 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 时,我们会重复使用故事 Promise,因此 story.json 只会提取一次。耶!Promise!

错误处理

如前所述,then() 接受两个参数,一个用于成功,一个用于失败(或用承诺术语表示,一个用于执行,一个用于拒绝):

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 回调。因此,如果上述任何操作都失败,系统会在页面中添加“未能显示章节”消息。

与 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 的实例,它只会返回该实例(注意:这是对规范的更改,某些实现尚未遵循此更改)。如果您向其传递类似 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”变量。系统会针对数组中的每个项调用我们的 reduce 回调。第一次调用时,“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。您会收到一个结果数组(无论这些 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';
})

与逐个加载相比,这种方法可能快几秒钟,并且所需的代码比我们的第一次尝试要少。章节可以按任意顺序下载,但在屏幕上显示时会按正确的顺序显示。

不过,我们仍然可以改进感知到的性能。第 1 章到货后,我们应该将其添加到该页面。这样,用户就可以在其余章节到达之前开始阅读。当第 3 章发布时,我们不会将其添加到该页面,因为用户可能不会意识到第 2 章缺失。当第二章推出时,我们可以添加第二章和第三章,以此类推。

为此,我们会同时提取所有章节的 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 的全部内容,将 Promise 与其他 ES6 功能结合使用,可以让 Promise 的使用变得更加简单。

加分赛:扩展的功能

自从我最初撰写本文以来,使用 Promise 的功能已大大扩展。从 Chrome 55 开始,异步函数允许以同步方式编写基于 Promise 的代码,但不会阻塞主线程。您可以参阅我的“异步函数”一文,详细了解相关内容。主要浏览器广泛支持 Promise 和异步函数。如需了解详情,请参阅 MDN 的 Promise异步函数参考文档。

非常感谢 Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、Addy Osmani、Arthur Evans 和 Yutaka Hirano 帮忙校对并提出了修改建议。

此外,感谢 Mathias Bynens 更新了本文的各个部分