2023 年 4 月 6 日

Promise 链式调用

让我们回到 简介:回调 一章中提到的问题:我们有一系列异步任务需要依次执行——例如,加载脚本。我们如何才能很好地编写代码?

Promise 提供了几种方法来实现此目的。

在本章中,我们将介绍 Promise 链式调用。

它看起来像这样

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

其思想是将结果通过 .then 处理程序的链传递。

这里的流程是

  1. 初始 Promise 在 1 秒内解析 (*)
  2. 然后,将调用.then处理程序(**),它又会创建一个新的 Promise(使用2值解决)。
  3. 下一个then(***)获取上一个的结果,对其进行处理(加倍),然后将其传递给下一个处理程序。
  4. …依此类推。

随着结果沿着处理程序链传递,我们可以看到一系列alert调用:124

整个过程之所以有效,是因为每次对.then的调用都会返回一个新的 Promise,这样我们就可以对其调用下一个.then

当处理程序返回一个值时,它将成为该 Promise 的结果,因此将使用该值调用下一个.then

一个经典的新手错误:从技术上讲,我们还可以向一个 Promise 添加多个.then。这不是链接。

例如

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

我们在这里所做的只是向一个 Promise 添加了几个处理程序。它们不会将结果相互传递;而是独立地对其进行处理。

以下是图片(将其与上面的链接进行比较)

同一个 Promise 上的所有.then都获取相同的结果 - 该 Promise 的结果。因此,在上面的代码中,所有alert都显示相同的内容:1

在实践中,我们很少需要为一个 Promise 设置多个处理程序。链接的使用频率要高得多。

返回 Promise

用于.then(handler)中的处理程序可以创建并返回一个 Promise。

在这种情况下,进一步的处理程序将等待它解决,然后获取其结果。

例如

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {

  alert(result); // 1

  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) { // (**)

  alert(result); // 2

  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) {

  alert(result); // 4

});

这里,第一个.then在行(*)中显示1并返回new Promise(…)。一秒钟后,它会解决,并且结果(resolve的参数,这里为result * 2)将传递给第二个.then的处理程序。该处理程序位于行(**)中,它显示2并执行相同操作。

因此,输出与上一个示例中的相同:1 → 2 → 4,但现在alert调用之间有 1 秒的延迟。

返回 Promise 允许我们构建异步操作链。

示例:loadScript

让我们将此功能与承诺的loadScript一起使用,该功能在上一章中定义,以按顺序逐个加载脚本

loadScript("/article/promise-chaining/one.js")
  .then(function(script) {
    return loadScript("/article/promise-chaining/two.js");
  })
  .then(function(script) {
    return loadScript("/article/promise-chaining/three.js");
  })
  .then(function(script) {
    // use functions declared in scripts
    // to show that they indeed loaded
    one();
    two();
    three();
  });

使用箭头函数可以使此代码更短。

loadScript("/article/promise-chaining/one.js")
  .then(script => loadScript("/article/promise-chaining/two.js"))
  .then(script => loadScript("/article/promise-chaining/three.js"))
  .then(script => {
    // scripts are loaded, we can use functions declared there
    one();
    two();
    three();
  });

此处,每个 loadScript 调用都会返回一个 Promise,并且下一个 .then 会在它解析时运行。然后它会启动下一个脚本的加载。因此,脚本会一个接一个地加载。

我们可以向链中添加更多异步操作。请注意,代码仍然是“扁平”的——它向下增长,而不是向右增长。没有“厄运金字塔”的迹象。

从技术上讲,我们可以像这样直接向每个 loadScript 添加 .then

loadScript("/article/promise-chaining/one.js").then(script1 => {
  loadScript("/article/promise-chaining/two.js").then(script2 => {
    loadScript("/article/promise-chaining/three.js").then(script3 => {
      // this function has access to variables script1, script2 and script3
      one();
      two();
      three();
    });
  });
});

此代码执行相同操作:按顺序加载 3 个脚本。但它“向右增长”。因此,我们遇到了与回调相同的问题。

开始使用 Promise 的人有时不知道链式调用,所以他们会这样写。通常,首选链式调用。

有时直接编写 .then 是可以的,因为嵌套函数可以访问外部作用域。在上面的示例中,最嵌套的回调可以访问所有变量 script1script2script3。但这是一种例外,而不是规则。

Thenable

准确地说,一个处理程序可能返回的不仅仅是一个 Promise,而是一个所谓的“thenable”对象——一个具有 .then 方法的任意对象。它将被视为 Promise 一样。

其理念是,第三方库可以实现它们自己的“与 Promise 兼容”的对象。它们可以有一组扩展的方法,但也可以与原生 Promise 兼容,因为它们实现了 .then

以下是一个 thenable 对象的示例

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // resolve with this.num*2 after the 1 second
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result); // (*)
  })
  .then(alert); // shows 2 after 1000ms

JavaScript 在行 (*) 中检查由 .then 处理程序返回的对象:如果它有一个名为 then 的可调用方法,那么它将调用该方法,提供原生函数 resolvereject 作为参数(类似于执行器),并等到其中一个被调用。在上面的示例中,resolve(2) 在 1 秒后被调用 (**)。然后,结果会进一步传递到链中。

此功能允许我们将自定义对象与 Promise 链集成,而无需从 Promise 继承。

更大的示例:fetch

在前端编程中,Promise 通常用于网络请求。因此,让我们看一个扩展的示例。

我们将使用 fetch 方法从远程服务器加载有关用户的信息。它有许多可选参数,在 单独的章节 中进行了介绍,但基本语法非常简单

let promise = fetch(url);

这会向 url 发出网络请求并返回一个 Promise。当远程服务器用标头响应时,Promise 会使用 response 对象解析,但在下载完整响应之前

要读取完整响应,我们应该调用方法 response.text():它返回一个 Promise,当从远程服务器下载完整文本时解析,结果为该文本。

下面的代码向 user.json 发出请求并从服务器加载其文本

fetch('/article/promise-chaining/user.json')
  // .then below runs when the remote server responds
  .then(function(response) {
    // response.text() returns a new promise that resolves with the full response text
    // when it loads
    return response.text();
  })
  .then(function(text) {
    // ...and here's the content of the remote file
    alert(text); // {"name": "iliakan", "isAdmin": true}
  });

fetch 返回的 response 对象还包括方法 response.json(),该方法读取远程数据并将其解析为 JSON。在我们的案例中,这更加方便,所以我们切换到它。

我们还将使用箭头函数来简化代码

// same as above, but response.json() parses the remote content as JSON
fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => alert(user.name)); // iliakan, got user name

现在让我们对加载的用户执行一些操作。

例如,我们可以向 GitHub 发出另一个请求,加载用户个人资料并显示头像

// Make a request for user.json
fetch('/article/promise-chaining/user.json')
  // Load it as json
  .then(response => response.json())
  // Make a request to GitHub
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  // Load the response as json
  .then(response => response.json())
  // Show the avatar image (githubUser.avatar_url) for 3 seconds (maybe animate it)
  .then(githubUser => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => img.remove(), 3000); // (*)
  });

代码有效;请参阅有关详细信息的注释。但是,其中存在一个潜在问题,对于那些开始使用 Promise 的人来说,这是一个典型的错误。

查看行 (*):如何在头像显示完毕并被移除之后执行操作?例如,我们希望显示一个用于编辑该用户或其他内容的表单。就目前而言,没有办法。

为了使链可扩展,我们需要返回一个在头像显示完毕时解决的 Promise。

像这样

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) { // (*)
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser); // (**)
    }, 3000);
  }))
  // triggers after 3 seconds
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));

也就是说,行 (*) 中的 .then 处理程序现在返回 new Promise,只有在 setTimeout (**) 中调用 resolve(githubUser) 之后,它才会解决。链中的下一个 .then 将等待它。

作为一个良好的实践,异步操作应始终返回一个 Promise。这使得在它之后规划操作成为可能;即使我们现在不打算扩展链,我们也可能在以后需要它。

最后,我们可以将代码拆分为可重用函数

function loadJson(url) {
  return fetch(url)
    .then(response => response.json());
}

function loadGithubUser(name) {
  return loadJson(`https://api.github.com/users/${name}`);
}

function showAvatar(githubUser) {
  return new Promise(function(resolve, reject) {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  });
}

// Use them:
loadJson('/article/promise-chaining/user.json')
  .then(user => loadGithubUser(user.name))
  .then(showAvatar)
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));
  // ...

总结

如果 .then(或 catch/finally,无关紧要)处理程序返回一个 Promise,则链的其余部分将等待它解决。当它解决时,它的结果(或错误)将进一步传递。

以下是完整图片

任务

这些代码片段是否相等?换句话说,在任何情况下,对于任何处理程序函数,它们的行为是否相同?

promise.then(f1).catch(f2);

promise.then(f1, f2);

简短的回答是:不,它们不相等

区别在于,如果在 f1 中发生错误,则它将在此处由 .catch 处理

promise
  .then(f1)
  .catch(f2);

…但不是在这里

promise
  .then(f1, f2);

这是因为错误会沿着链传递,而在第二个代码片段中,f1 下面没有链。

换句话说,.then 会将结果/错误传递到下一个 .then/catch。因此在第一个示例中,下面有一个 catch,而在第二个示例中没有,因此错误未得到处理。

教程地图

评论

在评论之前先阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 - 将其包装在 <pre> 标记中,对于 10 行以上 - 使用沙盒 (plnkrjsbincodepen…)