[译] 一个简单的 ES6 Promise 指南

The woods are lovely, dark and deep. But I have promises to keep, and miles to go before I sleep. — Robert Frostjavascript

Promise 是 JavaScript ES6 中最使人兴奋的新增功能之一。为了支持异步编程,JavaScript 使用了回调(callbacks),以及一些其余的技术。然而,使用回调会遇到地狱回调/末日金字塔等问题。Promise 是一种经过使代码看起来同步并避免在回调时出现问题进而大大简化异步编程的模式。html

在这篇文章中,咱们将看到什么是 Promise,以及如何利用它给咱们带来好处。前端

什么是 Promise?

ECMA 委员会将 promise 定义为 ——java

Promise 是一个对象,是一个用做延迟(也多是异步)计算的最终结果的占位符。node

简单来讲,一个 promise 是一个装有将来值的容器。若是你仔细想一想,这正是你正常的平常谈话中使用承诺(promise)这个词的方式。好比,你预约一张去印度的机票,准备前往美丽的山岗站大吉岭旅游。预订后,你会获得一张机票。这张机票是航空公司的一个承诺,意味着你在出发当天能够得到相应的座位。实质上,票证是将来值的占位符,即座位android

这还有另一个例子 —— 你向你的朋友承诺,你会在看完计算机程序设计艺术这本书后还给他们。在这里,你的话充当占位符。值就至关于这本书。ios

你能够想一想其余相似承诺(promise)的例子,这些例子涉及各类现实生活中的状况,例如在医生办公室等候,在餐厅点餐,在图书馆发放书籍等等。这些全部的状况都涉及某种形式的承诺(promise)。然而,例子只能告诉咱们这么多,Talk is cheap, so let’s see the code.git

建立 Promise

当某个任务的完成时间不肯定或太长时,咱们能够建立一个 promise 。例如 —— 根据链接速度的不一样,一个网络请求可能须要 10 ms 甚至须要 200 ms 这么久。咱们不想等待这个数据获取的过程。对你而言,200 ms 可能看起来不多,但对于计算机来讲是一段很是漫长的时间。promise 的目的就是让这种异步(asynchrony)变得简单而轻松。让咱们一块儿来看看基础知识。es6

使用 Promise 构造函数建立了一个新的 promise。像这样 ——github

const myPromise = new Promise((resolve, reject) => {
    if (Math.random() * 100 <= 90) {
        resolve('Hello, Promises!');
    }
    reject(new Error('In 10% of the cases, I fail. Miserably.'));
});
复制代码

Promise 示例

观察这个构造函数就能够发现其接收一个带有两个参数的函数,这个函数被称为执行器函数,而且它描述了须要完成的计算。执行器函数的参数一般被称为 resolvereject,分别标记执行器函数的成功和不成功的最终完成结果。

resolvereject 自己也是函数,它们用于将返回值返回给 promise 对象。当计算成功或将来值准备好时,咱们使用 resolve 函数将值返回。这时咱们说这个 promise 已经被成功解决(resolve)了

若是计算失败或遇到错误,咱们经过在 reject 函数中传递错误对象告知 promise 对象。 这时咱们说这个 promise 已经被拒绝(reject)了reject 能够接收任何类型的值。可是,建议传递一个 Error 对象,由于它能够经过查看堆栈跟踪来帮助调试。

在上面的例子中,Math.random() 用于生成一个随机数。有 90% 几率,这个 promise 会被成功解决(假设几率均匀分布)。其他的状况则会被拒绝。

使用 Promise

在上面的例子中,咱们建立了一个 promise 并将其存储在 myPromise 中。那咱们如何才能获取经过 resolve reject 函数传递过来的值呢?全部的 Promise 都有一个 .then() 方法。这样问题就好解决了,让咱们一块儿来看一下 ——

const myPromise = new Promise((resolve, reject) => {
    if (Math.random() * 100 < 90) {
        console.log('resolving the promise ...');
        resolve('Hello, Promises!');
    }
    reject(new Error('In 10% of the cases, I fail. Miserably.'));
});

// 两个函数
const onResolved = (resolvedValue) => console.log(resolvedValue);
const onRejected = (error) => console.log(error);

myPromise.then(onResolved, onRejected);

// 效果同上,代码更加简明扼要
myPromise.then((resolvedValue) => {
    console.log(resolvedValue);
}, (error) => {
    console.log(error);
});

// 有 90% 的几率输出下面语句

// resolving the promise ...
// Hello, Promises!
// Hello, Promises!
复制代码

使用 Promise

.then() 接收两个回调函数。第一个回调在 promise 被解决时调用。第二个回调在 promise 被拒绝时调用。

两个函数分别在第 10 行和第 11 行定义,即 onResolvedonRejected。它们做为回调传递给第 13 行中的 .then()。你也可使用第 16 行到第 20 行更常见的 .then 写做风格。它提供了与上述写法相同的功能。

在上面的例子中还有一些须要注意的重要事项。

咱们建立了一个 promise 实例 myPromise。咱们分别在第 13 行和第 16 行附加了两个 .then 的处理程序。尽管它们在功能上是相同的,但它们仍是被被视为不一样的处理程序。可是 ——

  • 一个 promise 只能成功(resolved)或失败(reject)一次。它不能成功或失败两次,也不能从成功切换到失败,反之亦然。
  • 若是一个 promise 在你添加成功/失败回调(即 .then)以前就已经成功或者失败,则 promise 仍是会正确地调用回调函数,即便事件发生地比添加回调函数要早。

这意味着一旦 promise 达到最终状态,即便你屡次附加 .then 处理程序,状态也不会改变(即不会再从新开始计算)。

为了验证这一点,你能够在第3行看到一个 console.log 语句。当你用 .then 处理程序运行上述代码时,须要输出的语句只会被打印一次。它代表 promise 缓存告终果,而且下次也会获得相同的结果

另外一个要注意的是,promise 的特色是及早求值(evaluated eagerly)只要声明并将其绑定到变量,就当即开始执行。没有 .start.begin 方法。就像在上面的例子中那样。

为了确保 promise 不是当即开始而是惰性求值(evaluates lazily),咱们将它们包装在函数中。稍后会看到一个例子。

捕捉 Promise

到目前为止,咱们只是很方便地看到了 resolve 的案例。那当执行器函数发生错误的时候会发生什么呢?当发生错误时,执行 .then() 的第二个回调,即 onRejected。让咱们来看一个例子 ——

const myProimse = new Promise((resolve, reject) => {
  if (Math.random() * 100 < 90) {
    reject(new Error('The promise was rejected by using reject function.'));
  }
  throw new Error('The promise was rejected by throwing an error');
});

myProimse.then(
  () => console.log('resolved'), 
  (error) => console.log(error.message)
);

// 有 90% 的几率输出下面语句

// The promise was rejected by using reject function.
复制代码

Promise 出错

这与第一个例子相同,但如今它以 90% 的几率执行 reject 函数,而且剩下的 10% 的状况会抛出错误。

在第 10 和 11 行,咱们分别定义了 onResolvedonRejected 回调。请注意,即便发生错误,onRejected 也会执行。所以咱们没有必要经过在 reject 函数中传递错误来拒绝一个 promise。也就是说,这两种状况下的 promise 都会被拒绝。

因为错误处理是健壮程序的必要条件,所以 promise 为这种状况提供了一条捷径。当咱们想要处理一个错误时,咱们可使用 .catch(onRejected) 接收一个回调:onRejected,而没必要使用 .then(null, () => {...})。如下代码将展现如何使用 catch 处理程序 ——

myProimse.catch(  
  (error) => console.log(error.message)  
);
复制代码

请记住 .catch 只是 .then(undefined, onRejected) 的一个语法糖

Promise 链式调用

.then().catch() 方法老是返回一个 promise。因此你能够把多个 .then 连接到一块儿。让咱们经过一个例子来理解它。

首先,咱们建立一个返回 promise 的 delay 函数。返回的 promise 将在给定秒数后解析。这是它的实现 ——

const delay = (ms) => new Promise(  
  (resolve) => setTimeout(resolve, ms)  
);
复制代码

在这个例子中,咱们使用一个函数来包装咱们的 promise,以便它不会当即执行。该 delay 函数接收以毫秒为单位的时间做为参数。因为闭包的特色,该执行器函数能够访问 ms 参数。它还包含一个在 ms 毫秒后调用 resolve 函数的 setTimeout 函数,从而有效解决 promise。这是一个示例用法 ——

delay(5000).then(() => console.log('Resolved after 5 seconds'));
复制代码

只有在 delay(5000) 解决后,.then 回调中的语句才会运行。当你运行上面的代码时,你会在 5 秒后看到 Resolved after 5 seconds 被打印出来。

如下是咱们如何实现 .then() 的链式调用 ——

const delay = (ms) => new Promise(
  (resolve) => setTimeout(resolve, ms)
);

delay(2000)
  .then(() => {
    console.log('Resolved after 2 seconds')
    return delay(1500);
  })
  .then(() => {
    console.log('Resolved after 1.5 seconds');
    return delay(3000);
  }).then(() => {
    console.log('Resolved after 3 seconds');
    throw new Error();
  }).catch(() => {
    console.log('Caught an error.');
  }).then(() => {
    console.log('Done.');
  });

// Resolved after 2 seconds
// Resolved after 1.5 seconds
// Resolved after 3 seconds
// Caught an error.
// Done.
复制代码

Promise 链式调用

咱们从第 5 行开始。所采起的步骤以下 ——

  • delay(2000) 函数返回一个在两秒以后能够获得解决的 promise。
  • 第一个 .then() 执行。它输出了一个句子 Resolved after 2 seconds。而后,它经过调用 delay(1500) 返回另外一个 promise。若是一个 .then() 里面返回了一个 promise,该 promise 的**解决方案(技术上称为结算)**是转发给下一个 .then 去调用。
  • 链式调用持续到最后。

另请注意第 15 行。咱们在 .then 里面抛出了一个错误。那意味着当前的 promise 被拒绝了,并被下一个 .catch 处理程序捕捉。所以,Caught an error 这句话被打印。然而,一个 .catch 自己老是被解析为 promise,而且不会被拒绝(除非你故意抛出错误)。这就是为何 .then 后面的 .catch 会被执行的缘由。

这里建议使用 .catch 而不是带有 onResolvedonRejected 参数的 .then 去处理。下面有一个案例解释了为何最好这样作 ——

const promiseThatResolves = () => new Promise((resolve, reject) => {
  resolve();
});

// 致使被拒绝的 promise 没有被处理
promiseThatResolves().then(
  () => { throw new Error },
  (err) => console.log(err),
);

// 适当的错误处理
promiseThatResolves()
  .then(() => {
    throw new Error();
  })
  .catch(err => console.log(err));
复制代码

第 1 行建立了一个始终能够解决的 promise。当你有一个带有两个回调 ,即 onResolvedonRejected.then 方法时,你只能处理执行器函数的错误和拒绝。假设 .then 中的处理程序也会抛出错误。它不会致使执行 onRejected 回调,如第 6 - 9 行所示。

但若是你在 .then 后跟着调用 .catch,那么 .catch 既捕捉执行器函数的错误也捕捉 .then 处理程序的错误。这是有道理的,由于 .then 老是返回一个 promise。如第 12 - 16 行所示。


你能够执行全部的代码示例,并经过实践应用学的更多。一个好的学习方法是将 promise 经过基于回调的函数从新实现。若是你使用 Node,那么在 fs 和其余模块中的不少函数都是基于回调的。在 Node 中确实存在能够自动将基于回调的函数转换为 promise 的实用工具,例如 util.promisifypify。可是,若是你还在学习阶段,请考虑遵循 WET(Write Everything Twice)原则,并从新实现或阅读尽量多的库/函数的代码。若是不是在学习阶段,特别是在生产环境下,请每隔一段时间就要使用 DRY(Don’t Repeat Yourself) 原则激励本身。

还有不少其余的 promise 相关知识我没有说起,好比 Promise.allPromise.race 和其余静态方法,以及如何处理 promise 中出现的错误,还有一些在建立一个promise 时应该注意的一些常见的反模式(anti-patterns)和细节。你能够参考下面的文章,以即可以更好地了解这些主题。

若是你但愿我在另外一篇文章中涵盖这些主题,请回复本文!:)


参考

我但愿你能喜欢这个客串贴!本文由 Arfat Salmon 专门为 CodeBurst.io 撰写

结束语

感谢阅读!若是你最终决定走上 web 开发这条不归路,请查看:2018 年 Web 开发人员路线图

若是你正在努力成为一个更好的 JavaScript 开发人员,请查看:提升你的 JavaScript 面试水平 ——  学习算法 + 数据结构

若是你但愿成为我每周一次的电子邮件列表中的一员,请考虑在此输入你的 email,或者在 Twitter 上关注我。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索