在JavaScript中使用Promises时最常见的3个错误

Promise 在 JavaScript 中有着举足轻重的地位。即使如今引入了 async/await ,Promise 对 JS 开发人员来说仍旧是不可或缺的。

但是 JavaScript 在处理异步性上与其他编程语言是不同的。 因此,即使具有丰富经验的开发人员有时也会犯错。我曾看到过一些厉害的 Python 或 Java 程序员在为 Node.js 或浏览器编码时犯了很初级的错误。

为了避免那些错误,Promise 做了很多细节处理。有些只是编码风格上的处理,但是许多处理方式都会引入一些真实存在却又难以跟踪的错误。因此,我决定编写一份清单,列出我所见过的 Promise 开发人员常犯的三个错误。

1. 将所有内容放在Promise构造函数中

这个错误虽然显而易见,但开发人员经常犯。

当你刚学 Promise 的时候,会学习 Promise 构造函数,它用于新建 Promise 对象。
可能好多人刚学 Promise 的时候,看到很多例子都是将将一些浏览器 API(例如 setTimeout )包装在 Promise 构造函数中,所以在他们心中根深蒂固地认为创建 Promise 对象的唯一方法是使用构造函数。
因此,他们经常写这样的代码:

const createdPromise = new Promise(resolve => {
  somePreviousPromise.then(result => {
    // do something with the result
    resolve(result);
  });
});

可以看到,为了可以对来自somePreviousPromise的结果result变量进行操作,上述代码调用了then函数,但是将它再次放进了一个 Promise 构造函数中,以便能在createdPromise变量中存储计算结果,这大概是为了以后能在createdPromise中进行更多操作。

当然这是不必要的。 then方法本身可以返回一个 Promise 对象,它表示执行somePreviousPromise的构造函数,然后在somePreviousPromise解析得到一个值后,将该值作为参数传递给then的回调函数,之后执行该回调函数。

因此,上述代码大概相当于:

const createdPromise = somePreviousPromise.then(result => {
  // do something with result
  return result;
});

好多了,是不是?

但是为什么说上面两段代码是大致相等呢? 区别在哪里?

不熟悉 Promise 的开发人员可能很难发现,但实际上两者在错误处理方面存在巨大差异,从这点来说,第一段代码虽然看起来丑陋冗长,但这似乎已经算是微不足道的缺点了。

假设somePreviousPromise失败了并抛出错误,原因可能是 Promise 在下面发出一个 HTTP 请求,而 API 返回编号500的错误。

在第一块代码中,我们将一个 Promise 包装到另一个 Promise 中,这根本无法捕获该错误。 为了解决这个问题,这部分代码必须进行以下更改:

const createdPromise = new Promise((resolve, reject) => {
 somePreviousPromise.then(result => {
   // do something with the result
   resolve(result);
 }, reject);
});

我们添加了一个reject参数作为第二个回调函数,然后将其作为第二个参数传递给then方法来使用它。请务必记住,then方法接受第二个可选参数来进行错误处理,这一点非常重要。

这样可以解决所有问题吗? 还是不行。

我们处理了somePreviousPromise本身可能发生的错误,但是我们仍然无法控制传递给then方法的第一个回调函数中发生的情况。// do something with the result这块代码执行时也可能会有一些错误。 不管这块代码抛出什么类型的错误,then方法的第二个参数的reject都无法捕获。

这是因为错误处理函数reject作为第二个参数传递给它,所以这个reject函数只能对方法链中较早发生的错误作出反应。

因此,这段代码正确的(也是最终的)版本为:

const createdPromise = new Promise((resolve, reject) => {
  somePreviousPromise.then(result => {
    // do something with the result
    resolve(result);
  }).catch(reject);
});

请注意,这次我们使用了catch方法,因为它是在第一个方法之后调用的,所以它将捕获在其上方的链中抛出的所有错误。 因此,不管somePreviousPromise还是then的回调失败的时候catch都能捕捉到。

正如上述分析的,在 Promise 构造函数中包装代码时,有很多微妙之处。 这就是为什么最好使用then方法创建新的 Promises的 原因,如第二段代码所示,它不仅更方便阅读,而且还会避免那些极端情况。

2. 连续的then方法 vs 并行的then方法

由于许多程序员都有面向对象的编程背景,因此自然而然地,他们会将方法转成一个对象而不是创建一个新的对象。

这可能就是为什么有些人不清楚在 Promise 上调用then方法时到底发生了什么的原因。

比较下面两段代码:

const somePromise = createSomePromise();

somePromise
  .then(doFirstThingWithResult)
  .then(doSecondThingWithResult);
const somePromise = createSomePromise();

somePromise
  .then(doFirstThingWithResult);

somePromise
  .then(doSecondThingWithResult);

上面两段代码做了同样的事情吗?看起来似乎是的。毕竟,两段代码在somePromise上都涉及到了两次调用then,对吗?

不,这是一个很常见的误区。实际上,这两段代码做的事完全不同。如果不完全了解这两段代码执行时到底发生了什么,可能会出现一些棘手的错误。

正如在上个部分提到的,then 方法会返回一个全新的、独立的 Promise。这意味着在第一段代码中,第二个then方法不是somePromise调用的,而是在then方法返回的这个新的 Promise 对象上调用第二个then,这个新的 Promise 对象等待somePromise变成 resolved 状态,然后立即调用doFirstThingWithResult。然后,给这个新的 Promise 实例添加doSecondThingWithResult回调函数。

实际上,第一段代码中,这两个回调函数将一个接一个地执行,这保证了只有在第一个回调函数执行完且没有任何问题之后才调用第二个回调函数。此外,第一个回调函数将获得somePromise返回的值作为参数,但是第二个回调将获得doFirstThingWithResult函数返回的值作为参数。

而在第二段代码中,让somePromise上调用了两次then方法,基本上忽略了两个then方法返回的新的 Promise 对象。由于then在完全相同的 Promise 实例上被调用了两次,因此无法保证首先执行哪个回调函数。这里的执行顺序是不确定的。

从某种意义上说,第二段代码的这两个回调函数应该是独立的,并且不依赖于任何先前调用的回调函数,我有时将其视为“并行”执行。但是,实际上 JS 引擎一次只能执行一个函数,你根本不知道会以什么顺序调用它们。

第二个区别是第二段代码中的doFirstThingWithResultdoSecondThingWithResult都将接收相同的参数— somePromise内部执行resolve函数时传递的参数。在这段代码中,两个回调函数的返回值都将被完全忽略。

3. 创建后立即执行 Promise

这个错误还是与大多数编码人员通常在面向对象编程中经验丰富这点有关。

编程规范通常要求确保对象的构造函数自身不执行任何操作。例如,表示数据库的对象在使用 new 关键字调用其构造函数时不应启动与数据库的连接。

相反,最好提供一个特殊的方法(例如名为 init 的方法),以明确创建连接。这样,对象不会在创建时执行任何意外行为。它耐心地等待程序员明确要求执行的方法。

但是 Promise 不是这样的。

看下面这个例子:

const somePromise = new Promise(resolve => {
  // make HTTP request
  resolve(result);
});

你可能会认为发出HTTP请求的函数未在此处调用,因为它包装在 Promise 构造函数中。实际上,许多程序员期望仅在somePromise上执行then方法之后才调用它。

但事实并不是这样的。创建该 Promise 后,回调将立即执行。这意味着当代码执行到创建somePromis变量的下一行时,你的 HTTP 请求可能已经在执行或至少计划要执行了。

我们说 Promise 很“急切“,因为它会尽可能快地执行相关操作。相比之下,许多人期望 Promises 可以是“懒惰的”,即仅在绝对必要时(例如,在 Promise 对象第一次调用then时)才执行操作。这是一个误区,Promise 永远是“急切的”而不是“懒惰的”。

但是,如果想以后再执行 Promise 应该怎么做?如果希望推迟发出该 HTTP 请求怎么办?Promise 中是否内置了某种可以执行类似操作的机制?

要实现上述操作其实很简单。由于函数是一种惰性机制,仅当程序员使用()括号语法显式调用它们时,才执行它们。仅仅定义一个函数并不能真正做任何事情。因此,使 Promise 变得“懒惰”的最佳方法是将其简单地包装在函数中!

像这样:

const createSomePromise = () => new Promise(resolve => {
  // make HTTP request
  resolve(result);
});

现在,我们将相同的 Promise 构造函数调用包装在一个函数中。因此,还没有真正调用它。我们还将变量名从somePromise更改为createSomePromise,因为它不再是 Promise 对象了,而是一个创建并返回 Promise 对象的函数。

Promise 构造函数(以及带有HTTP请求的回调函数)仅在执行createSomePromise时被调用。因此,现在我们相当于让 Promise 也具备了惰性机制,只有在我们真正想要执行时才执行它。

此外,请注意,这样写还额外提供了另一种功能,我们可以轻松地创建另一个执行相同动作的 Promise。

如果出于某种奇怪的原因,我们希望发起两次相同的 HTTP 请求并同时执行,则可以调用两次createSomePromise函数,即一次接一次。或者,如果请求由于任何原因失败,我们可以使用相同的函数重新发起请求。

这表明将 Promises 封装在函数(或方法)中非常方便,因此 JavaScript 开发人员应该习惯这种模式。

讽刺的是,如果你阅读了我另一篇关于 “Promises vs Observables” 的文章,你就会知道 Rx.js 的程序员经常会犯一个相反的错误。他们对 Observable 进行编码,希望它是“急切的”(像 Promises ),而实际上它是“懒惰的”。因此,将 Observables 封装在函数或方法中通常没有任何意义,甚至可能导致bug。

总结

本文介绍了三种类型的错误,这些错误是我经常看到的那些对 Promises 了解不深的开发者会犯的。

你也可以在评论中分享你遇到过的错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值