3

重学JavaScript Promise API - chuckQu

 1 year ago
source link: https://www.cnblogs.com/chuckQu/p/17618046.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在这篇教程中,我们将掌握如何在JavaScript中创建并使用Promise。我们将了解Promise链式调用、错误处理以及最近添加到语言中的一些Promise静态方法。

什么是Promise?

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

Promise是一个特殊的JavaScript对象,它代表了异步操作的最终结果。它就像操作结果的代理。

在拥有JavaScript Promise之前,处理异步操作最优雅的方式是使用回调。当异步操作的结果就绪时,回调就是一个运行的函数。比如说:

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,然后等待动画完成。或者,我们的服务器可能从客户端接收输入,验证输入,更新数据库,写入日志文件,最后发送响应。在这两种情况下,我们还需要处理发生的任何错误。

使用嵌套回调来完成这样的任务是非常痛苦的。幸运的是,Promise为我们提供了一种更简洁的语法,使我们能够将异步命令串联起来,让它们一个接一个地运行。

创建Promise

创建Promise的基本语法如下:

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

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

如果一切运行成功,则通过调用 resolve 来实现Promise。如果出现错误,则调用 reject 拒绝Promise。我们可以向这两个方法传递值,这些值将在消费代码中可用。

要了解这在实践中是如何工作的,请参考下面的代码。该代码向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构造函数创建一个新的Promise对象。该构造函数用于封装尚未支持Promise的函数或API,例如上面的XMLHttpRequest对象。传递给Promise构造函数的回调包含用于从远程服务获取数据的异步代码。(注意,我们在这里使用的是箭头函数)在回调中,我们向 https://icanhazdadjoke.com/ 创建了一个 Ajax 请求,该请求以 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解决(即fulfilledrejected)时调用。如果Promise实现,成功回调将使用我们传递给resolve的实际数据触发。如果Promise被拒绝,失败回调将被调用。无论我们传递给reject的是什么,都将作为参数传递给该回调。

Promise的状态

在上面代码中,我们可以通过调用resolvereject方法来改变Promise的状态。在继续之前,花点时间看下Promise的生命周期。

Promise的状态会是下面值的其中一种:

  • pending
  • fulfilled
  • rejected
  • settled

Promise开始时处于pending的状态。这意味着它既没有fulfilled也没有rejected。如果与Promise相关的操作成功(在我们的示例中是远程 API 调用),并且调用了 resolve 方法,那么Promise称为fulfilled。另一方面,如果相关操作不成功,且 reject 方法被调用,则该Promise处于rejected状态。最后,如果Promise处于fulfilledrejected状态,但不是pending状态,则称为settled

image.png

一旦Promise是rejected或者fulfilled,该状态将永久与之关联。这意味着Promise只能成功或失败一次。如果Promise已经fulfilled,并且在其后附加有两个回调的then(),那么成功回调会直接被调用。因此,在Promise的世界里,我们不关心Promise何时settled。我们只关心Promise的最终结果。

Promise链式调用

有时可能需要将多个异步任务按照特定顺序链在一起。这就是所谓的Promise链式调用。让我们重温一下 setTimeout 示例,以了解Promise链式调用的基本工作原理。

我们可以像以前一样,首先创建一个新的Promise对象:

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

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

不出所料,Promise在一秒后被执行,控制台打印"1"。

为了继续链式调用,我们需要在控制台语句后返回第二个Promise,并将其传递给第二个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);
});

虽然这个方法可行,但它已经开始变得有点笨重。让我们创建一个返回新Promise的函数,并在特定时间后解析该Promise:

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实现一个sleep函数,你可能对这篇文章感兴趣。

向下传递数据

当我们需要执行多个异步操作时,我们可能希望将一个异步调用的结果传递给Promise链中的下一个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 调用返回的Promise,服务器的响应 (res) 在下面的 then 中可用。

Promise错误处理

我们已经知道,then函数接收两个回调函数作为参数,并且如果Promise被拒绝,第二个参数会被调用:

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

然而,为每个Promise指定错误处理函数是相当繁琐的,尤其是处理Promise链式调用的时候。幸运的是,还有更好的方式。

catch方法

我们还可以使用catch方法,它可以为我们处理错误。当一个Promise在Promise链的任何地方rejected时,控制会跳转到最近的拒绝处理函数中。这非常方便,因为它意味着我们可以在链的末尾添加一个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));

注意,除了在代码块的末尾添加错误处理函数之外,我还在第7行将res.json()拼错为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 settled后运行,也就是resolved或者rejected。与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');

它不接收任何参数并返回一个Promise,因此我们可以在它的返回值上链式调用更多的thencatchfinally调用。

更多Promise方法

到此为止,我们已经对JavaScript Promise有了一个很好的基本了解,但在结束之前,我们需要注意各种Promise实用方法。

Promise.all()

在前面的示例中,我们需要在第一个 Ajax 调用完成后才能进行第二个 Ajax 调用。与此不同的是,有时我们会有一堆完全不相互依赖的异步操作。这时就需要使用 Promise.all

该方法接收一个Promise数组,等待所有Promise resolved或其中任何一个Promise rejected。如果所有的Promise都成功resolvedall实现一个数组,该数组包含各个Promise的履行值:

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]

然而,如果任何Promise rejectedall将拒绝该Promise的值,而不会考虑任何其他Promise。

Promise.allSettled()

不像allPromise.allSettled 将等待传递给它的每一个Promise的实现或拒绝。如果一个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));

这将返回状态和值(如果fulfilled)或者原因(如果rejected)的列表:

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

Promise.any()

Promise.any()返回第一个状态为fulfilled的Promise的值。如果有任何Promise rejected,都会被忽略:

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

在1.5秒后,控制台会打印"2"。

Promise.race()

Promise.race也接收一个Promise数组,并(像上面列出的其他方法一样)返回一个新的Promise。只要它接收到的一个Promise实现或者拒绝,race本身就会使用刚刚settled的Promise的值或原因来实现或拒绝:

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”,因为数组中的第一个Promise会被立即拒绝,并且拒绝会被我们的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”。

这两个例子中,其他两个Promise都会被忽略。

应该使用哪个

到目前为止,我们已经了解了回调和Promise,但值得一提的还有较新的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。我们学习了如何创建一个Promise链,并将数据从一个异步操作传递到下一个异步操作。我们还研究了错误处理以及各种Promise实用方法。

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

以上就是本文的全部内容。如果对你有所帮助,欢迎点赞、收藏、转发~


Recommend

  • 43
    • 掘金 juejin.im 4 years ago
    • Cache

    重学JavaScript之window对象

    重学JavaScript之window对象 欢迎关注 前端公众号【小夭同学】 ECMAScript是Ja...

  • 9
    • www.cnblogs.com 2 years ago
    • Cache

    React技巧之导入组件 - chuckQu

    正文从这开始~ 在React中,从其他文件中导入组件: 从A文件中导出组件。比如说,export function Button() {} 。 在B文件中导入组件。比如说,import {Button} from './another-file'

  • 9
    • www.cnblogs.com 2 years ago
    • Cache

    React技巧之中断map循环 - chuckQu

    React技巧之中断map循环 正文从这开始~ 在Reac...

  • 3

    每个JavaScript环境都有一个全局对象(global object)。在全局范围内创建的任何变量实际上都是这个对象的属性,而任何函数都是它的方法。在浏览器环境中,全局对象是window对象,它代表了包含网页的浏览器窗口。 在这篇文章中,我们将介绍

  • 1

    你如何确定一个JavaScript原生函数是否被覆盖? 你不能--或者至少无法可靠地确定。有一些检测方法很接近,但你不能完全相信它们。 JavaScript原生函数 在JavaScript中,原生函数指的是其源代码...

  • 5
    • www.cnblogs.com 1 year ago
    • Cache

    JavaScript中的箭头函数 - chuckQu

    本文可以让你了解所有有关JavaScript箭头函数的信息。我们将告诉你如何使用ES6的箭头语法,以及在代码中使用箭头函数时需要注意的一些常见错误。你会看到很多例子来说明它们是如何工作的。 JavaScript的箭头函数随着ECMAScript 2015的发布而到来,也被称为ES6...

  • 9

    循环允许我们通过循环数组或对象中的项并做一些事情,比如说打印它们,修改它们,或执行其他类型的任务或动作。JavaScript有各种各样的循环,for循环允许我们对一个集合(如数组)进行迭代。 在这篇文章中,我们将了解JavaScript提供的for...

  • 2

    Introducing the WebAssembly JavaScript Promise Integration APIPublished 19 January 2023 · Tagged with WebAssemblyThe JavaScript Promis...

  • 6
    • www.cnblogs.com 1 year ago
    • Cache

    JavaScript 如何验证 URL - chuckQu

    当开发者需要为不同目的以不同形式处理URL时,比如说浏览器历史导航,锚点目标,查询参数等等,我们经常会借助于JavaScript。然而,它的频繁使用促使攻击者利用其漏洞。这种被利用的风险是我们必须在我们的JavaScript应用程序中实现URL验证的原因。 URL验证检...

  • 4

    创建一个双模式跨运行时的 JavaScript 包 本文将指...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK