JavaScript承诺,本章将介绍JavaScript中创建和使用承诺。我们将研究链接承诺、错误处理以及添加到语言中的一些较新的承诺方法。

JavaScript承诺 什么是 JavaScript Promise?

在 JavaScript 中,一些操作是异步的。这意味着它们产生的结果或值在操作完成时不会立即可用。

Promise是一个特殊的 JavaScript 对象,它表示这种异步操作的最终结果。它充当操作结果的代理。

糟糕的过去:回调函数

在我们拥有 JavaScript 承诺之前,处理异步操作的首选方法是使用回调。回调是在异步操作的结果就绪时运行的函数。例如:

setTimeout(function() {
  console.log('Hello, World!');
}, 1000);

这里,setTimeout是一个异步函数,它在指定的毫秒数后运行它传递的任何回调函数。在这种情况下,它会记录“Hello, World!” 一秒钟后到控制台。

现在假设我们想要每秒记录一条消息,持续五秒钟。那看起来像这样:

setTimeout(function() {
  console.log(1);
  setTimeout(function() {
    console.log(2);
    setTimeout(function() {
      console.log(3);
      setTimeout(function() {
        console.log(4);
        setTimeout(function() {
          console.log(5);
        }, 1000);
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);

以这种方式使用多个嵌套回调的异步 JavaScript 既容易出错又难以维护。它通常被称为回调地狱

不可否认,这是一个人为的例子,但它可以说明这一点。在真实场景中,我们可能会进行 Ajax 调用,用结果更新 DOM,然后等待动画完成。或者,我们的服务器可能会从客户端接收输入、验证该输入、更新数据库、写入日志文件并最终发送响应。在这两种情况下,我们还需要处理发生的任何错误。

使用嵌套回调来完成此类任务会很痛苦。幸运的是,promises 为我们提供了一种更简洁的语法,使我们能够链接异步命令,以便它们一个接一个地运行。

如何创建 JavaScript Promise 对象

创建承诺的基本语法如下:

const promise = new Promise((resolve, reject) => {
  //asynchronous code goes here
});

我们首先使用Promise构造函数实例化一个新的 promise 对象,并向它传递一个回调函数。回调有两个参数,resolvereject,它们都是函数。我们所有的异步代码都在该回调中。

如果一切运行成功,承诺将通过调用来实现resolve。如果出现错误,promise 将被调用拒绝reject。我们可以将值传递给这两种方法,这些方法将在使用代码中可用。

要了解这在实践中是如何工作的,请考虑以下代码。这向 Web 服务发出一个异步请求,该服务以 JSON 格式返回一个随机的让人尴尬的低级错误:

const promise = new Promise((resolve, reject) => {
  const request = new XMLHttpRequest();
  request.open('GET', 'https://icanhazdadjoke.com/');
  request.setRequestHeader('Accept', 'application/json');

  request.onload = () => {
    if (request.status === 200) {
      resolve(request.response); // we got data here, so resolve the Promise
    } else {
      reject(Error(request.statusText)); // status is not 200 OK, so reject
    }
  };

  request.onerror = () => {
    reject(Error('Error fetching data.')); // error occurred, reject the  Promise
  };

  request.send(); // send the request
});

承诺构造器

我们首先使用Promise构造函数创建一个新的 promise 对象。构造函数用于包装尚不支持 promises 的函数或 API,例如XMLHttpRequest上面的对象。传递给 promise 构造函数的回调包含用于从远程服务获取数据的异步代码。(请注意,我们在这里使用了箭头函数。)在回调中,我们创建了一个 Ajax 请求到https://icanhazdadjoke.com/,它以 JSON 格式返回一个随机的爸爸笑话。

当从远程服务器收到成功的响应时,它会传递给该resolve方法。如果发生任何错误 – 无论是在服务器上还是在网络级别 –reject都会调用一个Error对象。

then方法_

当我们实例化一个 promise 对象时,我们得到了将来可用的数据的代理。在我们的例子中,我们期望从远程服务返回一些数据。那么,我们如何知道数据何时可用?这是Promise.then()使用函数的地方:

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

promise.then((data) => {
  console.log('Got data! Promise fulfilled.');
  document.body.textContent = JSON.parse(data).joke;
}, (error) => {
  console.error('Promise rejected.');
  console.error(error.message);
});

此函数可以采用两个参数:成功回调和失败回调。这些回调在 promise 确定(即完成或拒绝)时调用。如果承诺得到履行,成功回调将被触发,我们传递给的实际数据resolve。如果承诺被拒绝,将调用失败回调。无论我们传递给什么,reject都将作为参数传递给这个回调。

我们可以在下面的 CodePen 演示中试用这段代码。要查看新的随机笑话,请点击嵌入右下角的“重新运行”按钮。

 

JavaScript 承诺的状态是什么?

在上面的代码中,我们看到我们可以通过调用resolveorreject方法来更改承诺的状态。在我们继续之前,让我们花点时间看看 promise 的生命周期。

承诺可能处于以下状态之一:

  • 待办的
  • 履行
  • 拒绝
  • 定居

承诺以未决状态开始生活。这意味着它既没有实现也没有被拒绝。如果与 promise 相关的操作成功(在我们的例子中是远程 API 调用)并且调用了resolve方法,则称 promise 已实现。另一方面,如果相关操作不成功并且reject调用了该方法,则承诺处于拒绝状态。最后,如果一个 promise 被履行或被拒绝,但不是未决的,就被称为已解决。

JavaScript 承诺的四种状态

一旦一个承诺被拒绝或履行,这个状态就会永久地与之相关联。这意味着一个承诺只能成功或失败一次。如果 promise 已经实现,稍后我们then()用两个回调附加到它,成功回调将被正确调用。因此,在承诺的世界中,我们对了解承诺何时结算不感兴趣。我们只关心承诺的最终结果。

但是我们不应该使用 Fetch API 吗?

此时,我们可能会问为什么我们不使用Fetch API从远程服务器获取数据,答案是我们可能应该这样做。

XMLHttpRequest对象不同,Fetch API 是基于承诺的,这意味着我们可以像这样重写我们的代码(减去错误处理):

fetch('https://icanhazdadjoke.com', { 
  headers: { 'Accept': 'application/json' }
})
  .then(res => res.json())
  .then(json => console.log(json.joke));

使用的原因XMLHttpRequest是为了更深入地了解引擎盖下发生的事情。

链接承诺

有时可能需要以特定顺序将多个异步任务链接在一起。这称为承诺链。让我们重新审视我们的setTimeout示例,以了解 promise 链是如何工作的基本概念。

我们可以像之前那样创建一个新的 promise 对象:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => { resolve() }, 1000)
});

promise.then(() => {
  console.log(1);
});

正如预期的那样,承诺在一秒钟后得到解决,并且“1”被记录到控制台。

为了继续这个链条,我们需要在我们的控制台语句之后返回第二个承诺并将其传递给第二个then

const promise = new Promise((resolve, reject) => {
  setTimeout(() => { resolve() }, 1000)
});

promise.then(() => {
  console.log(1);
  return new Promise((resolve, reject) => {
    setTimeout(() => { resolve() }, 1000)
  });
}).then(() => {
  console.log(2);
});

虽然这有效,但它已经开始变得有点笨拙了。让我们创建一个返回新承诺的函数,该承诺会在特定时间过去后得到解决:

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

然后我们可以使用它来展平我们的嵌套代码:

sleep(1000)
  .then(() => {
    console.log(1);
    return sleep(1000);
  }).then(() => {
    console.log(2);
    return sleep(1000);
  }).then(() => {
    console.log(3);
    return sleep(1000);
  })
  ...

因为该then方法本身返回一个 promise 对象,并且我们没有将任何值从一个异步操作传递到下一个异步操作,这使我们能够进一步简化事情:

sleep(1000)
  .then(() => console.log(1))
  .then(() => sleep(1000))
  .then(() => console.log(2))
  .then(() => sleep(1000))
  .then(() => console.log(3))
  ...

这比原来的代码优雅多了。

请注意,如果您想了解有关在 JavaScript 中实现睡眠功能的更多信息,您可能会发现这很有趣:在 JavaScript 中延迟、睡眠、暂停和等待。

将数据传递到承诺链

当我们要执行多个异步操作时,我们可能希望将一个异步调用的结果传递给then承诺链中的下一个块,以便我们可以对该数据做一些事情。

例如,我们可能想要获取 GitHub 存储库的贡献者列表,然后使用此信息获取第一个贡献者的姓名:

fetch('https://api.github.com/repos/eslint/eslint/contributors')
  .then(res => res.json())
  .then(json => {
    const firstContributor = json[0].login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
  })
  .then(res => res.json())
  .then(json => console.log(`The first contributor to ESLint was ${json.name}`));

// The first contributor to ESLint was Nicholas C. Zakas

正如我们所见,通过返回从第二个 fetch 调用返回的承诺,服务器的响应 ( res) 在以下then块中可用。

承诺错误处理

我们已经看到该then函数将两个回调函数作为参数,如果承诺被拒绝,将调用第二个回调函数:

promise.then((data) => {
  console.log('Got data! Promise fulfilled.');
  ...
}, (error) => {
  console.error('Promise rejected.');
  console.error(error.message);
});

但是,在处理 promise 链时,为每个 promise 指定一个错误处理程序可能会变得非常冗长。幸运的是,有更好的方法……

catch方法_

我们也可以使用catch方法,它可以为我们处理错误。当 promise 在 promise 链中的任何地方拒绝时,控制权会跳转到最近的拒绝处理程序。这非常方便,因为这意味着我们可以将 a 添加catch到链的末尾并让它处理发生的任何错误。

我们以前面的代码为例:

fetch('https://api.github.com/repos/eslint/eslint/contributors')
  .then(res => res.json())
  .then(json => {
    const firstContributor = json[0].login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
  })
  .then(res => res.jsn())
  .then(json => console.log(`The top contributor to ESLint wass ${json.name}`))
  .catch(error => console.log(error));

请注意,除了在代码块末尾添加错误处理程序外,我还在第七行拼错res.json()了 as res.jsn

现在,当我们运行代码时,我们会在屏幕上看到以下输出:

TypeError: res.jsn is not a function
  <anonymous>  http://0.0.0.0:8000/index.js:7  
  promise callback*  http://0.0.0.0:8000/index.js:7  

index.js:9:27

我正在使用的文件名为index.js. 第 7 行包含错误,第 9 行是catch捕获错误的块。

finally方法_

Promise.finally方法在 promise 被解决时运行——也就是说,要么被解决要么被拒绝。与 一样catch,它有助于防止代码重复,并且对于执行清理任务非常有用,例如关闭数据库连接或从 UI 中删除加载微调器。

这是一个使用我们之前代码的示例:

function getFirstContributor(org, repo) {
  showLoadingSpinner();
  fetch(`https://api.github.com/repos/${org}/${repo}/contributors`)
  .then(res => res.json())
  .then(json => {
    const firstContributor = json[0].login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
  })
  .then(res => res.json())
  .then(json => console.log(`The first contributor to ${repo} was ${json.name}`))
  .catch(error => console.log(error))
  .finally(() => hideLoadingSpinner());
};

getFirstContributor('facebook', 'react');

它不接收任何参数并返回一个承诺,因此我们可以将更多 、 和 调用链接thencatchfinally的返回值上。

进一步的承诺方法

至此,我们已经对使用 JavaScript promises 有了很好的基本了解,但在我们结束之前,需要了解各种 promise 实用程序方法。

承诺.all()

与前面的示例不同,我们需要先完成第一个 Ajax 调用才能进行第二个调用,有时我们会有一堆异步操作,它们根本不依赖于彼此。这是Promise.all进来的时候。

此方法采用一系列承诺并等待所有承诺得到解决或其中任何一个被拒绝。如果所有承诺都成功解决,all则使用包含各个承诺的已实现值的数组来实现:

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

[1, 2, 3]上面的代码将在三秒后登录到控制台。

但是,如果任何承诺被拒绝,all将拒绝该承诺的价值,并且不会考虑任何其他承诺。

承诺.allSettled()

all,不同Promise.allSettled的是,它会等待传递给它的每个 promise 来履行或拒绝。如果承诺被拒绝,它不会停止执行:

Promise.allSettled([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
  new Promise((resolve, reject) => setTimeout(() => reject(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

这将返回状态和值列表(如果承诺已履行)或原因(如果被拒绝):

[
  { status: "fulfilled", value: 1 },
  { status: "rejected", reason: 2 },
  { status: "fulfilled", value: 3 },
]

承诺.any()

Promise.any返回要实现的第一个承诺的值。如果有任何承诺被拒绝,这些承诺将被忽略:

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(1), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

这将在一秒半后将“2”记录到控制台。

承诺.race()

Promise.race还接收一组承诺并(与上面列出的其他方法一样)返回一个新的承诺。一旦它收到的其中一个承诺履行或拒绝,race它自己就会履行或拒绝履行或拒绝刚刚结算的承诺的价值或原因:

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => reject('Rejected with 1'), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

这会将“Rejected with 1”记录到控制台,因为数组中的第一个承诺立即拒绝并且拒绝被我们的catch块捕获。

我们可以这样改变:

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve('Resolved with 1'), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

这会将“Resolved with 1”记录到控制台。

在这两种情况下,其他两个承诺都会被忽略。

JavaScript Promise 示例

接下来,让我们看一些实际的代码。这里有两个演示,它们汇集了我们在整篇文章中介绍的几个概念。

查找 GitHub 存储库的原始贡献者

第一个演示允许用户输入 GitHub 存储库的 URL。然后它将发出 Ajax 请求以检索该存储库的前 30 个贡献者的列表。当该请求完成时,它将发出第二个请求以检索原始贡献者的姓名并将其显示在页面上。为此,第二个获取调用使用第一个返回的数据。

为了演示 的使用finally,我在网络请求中添加了延迟,在此期间我显示了一个加载微调器。当请求完成时,这将被删除。

 

确定哪个 GitHub 存储库有更多的星号

在此示例中,用户可以输入两个 GitHub 存储库的 URL。然后该脚本将用于Promise.all并行发出两个请求以获取有关这些回购协议的一些基本信息。我们可以使用all,因为这两个网络请求完全相互独立。与前面的示例不同,一个的结果不基于另一个的结果。

一旦两个请求都完成,脚本将输出哪个 repo 有更多的星数,哪个 repo 的星数更少。

 

Promises、回调或异步……等待:我们应该使用哪一个?

到目前为止,我们已经了解了回调和承诺,但还值得一提的是更新的async ... await语法。虽然实际上只是 promise 之上的语法糖,但在许多情况下,它可以使基于 promise 的代码更易于阅读和理解。

例如,我们可以这样重写之前的代码:

async function getFirstContributor(org, repo) {
  showLoadingSpinner();
  try {
    const res1 = await  fetch(`https://apiy.github.com/repos/${org}/${repo}/contributors`);
    const contributors = await res1.json();
    const firstContributor = contributors[0].login;
    const res2 = await fetch(`https://api.github.com/users/${firstContributor}`)
    const details = await res2.json();
    console.log(`The first contributor to ${repo} was ${details.name}`);
  } catch (error) {
    console.error(error)
  } finally {
    hideLoadingSpinner();
  }
}

getFirstContributor('facebook', 'react');

可以看出,我们使用一种语法来处理错误,我们可以在块try ... catch内进行任何整理。finally

我发现上面的代码比基于 promise 的版本更容易解析。但是,我鼓励您熟悉async ... await语法并查看最适合您的语法。一个很好的起点是我们的文章现代 JavaScript 中的流控制,其中介绍了各种方法的许多优点和缺点。

混合使用这两种样式时也应该小心,因为错误处理有时会以意想不到的方式运行。基本上,promise 拒绝与异步错误不是一回事,这会给您带来麻烦,正如这篇文章所展示的那样。

结论

在本文中,我们了解了如何创建和使用 JavaScript 承诺。我们已经学习了如何创建承诺链并将数据从一个异步操作传递到下一个。我们还检查了错误处理以及各种承诺实用方法。

如上所述,下一步是开始学习async ... await并加深对 JavaScript 程序内部流控制的理解。

相关

如何避免JavaScirpt回调地狱

JavaScript闭包、回调和IIFE的揭秘

适合初学者的PHP书籍