Promise 链

23 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情

前言

当有一系列异步任务一个接一个执行,这时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;

});

image.png

如图所示,上面的代码取到result做三次倍乘。为何每次.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 的几个处理程序。它们不会相互传递 result;相反,它们之间彼此独立运行处理任务。也因此他们运行结果都是2

image.png

调用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(…)。传递给下一个.then的值就是新创建的promise的result(resolve 的参数,在这里它是 result*2),实际与第一个代码运行结果一致,逻辑类似。不同之处就是每次alert调用都有1s停顿

我们可以用箭头函数来重写代码,让其变得简短一些:

实例

loadScript("/article/promise-chaining/one.js")
  .then(script => loadScript("/article/promise-chaining/two.js"))
  .then(script => loadScript("/article/promise-chaining/three.js"))
  .then(script => {
    // 脚本加载完成,我们可以在这儿使用脚本中声明的函数
    one();
    two();
    three();
  });

在这儿,每个 loadScript 调用都返回一个 promise,并且在它 resolve 时下一个 .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 => {
      // 此函数可以访问变量 script1,script2 和 script3
      one();
      two();
      three();
    });
  });
});

这段代码做了相同的事儿:按顺序加载 3 个脚本。但它是“向右增长”的。所以会有和使用回调函数一样的问题。

更复杂的示例:fetch

在前端编程中,promise 通常被用于网络请求。那么,让我们一起来看一个相关的扩展示例吧。

基本语法:

let promise = fetch(url);

执行这条语句,向 url 发出网络请求并返回一个 promise。当远程服务器返回 header(是在 全部响应加载完成前)时,该 promise 使用一个 response 对象来进行 resolve。

为了读取完整的响应,我们应该调用 response.text() 方法:当全部文字内容从远程服务器下载完成后,它会返回一个 promise,该 promise 以刚刚下载完成的这个文本作为 result 进行 resolve。

下面这段代码向 user.json 发送请求,并从服务器加载该文本:

fetch('/article/promise-chaining/user.json')
  // 当远程服务器响应时,下面的 .then 开始执行
  .then(function(response) {
    // 当 user.json 加载完成时,response.text() 会返回一个新的 promise
    // 该 promise 以加载的 user.json 为 result 进行 resolve
    return response.text();
  })
  .then(function(text) {
    // ……这是远程文件的内容
    alert(text); // {"name": "iliakan", "isAdmin": true}
  });

从 fetch 返回的 response 对象还包含 response.json() 方法,该方法可以读取远程数据并将其解析为 JSON。在我们的例子中,这更加方便,所以我们用这个方法吧。

为了简洁,我们还将使用箭头函数:

// 同上,但使用 response.json() 将远程内容解析为 JSON
fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => alert(user.name)); // iliakan,获取到了用户名

现在,让我们用加载好的用户信息搞点事情。

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

// 发送一个对 user.json 的请求
fetch('/article/promise-chaining/user.json')
  // 将其加载为 JSON
  .then(response => response.json())
  // 发送一个到 GitHub 的请求
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  // 将响应加载为 JSON
  .then(response => response.json())
  // 显示头像图片(githubUser.avatar_url)3 秒(也可以加上动画效果)
  .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 时的典型问题。

请看 (*) 行:我们如何能在头像显示结束并被移除 之后 做点什么?例如,我们想显示一个用于编辑该用户或者其他内容的表单。就目前而言,是做不到的。

为了使链可扩展,我们需要返回一个在头像显示结束时进行 resolve 的 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);
  }))
  // 3 秒后触发
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));

也就是说,第 (*) 行的 .then 处理程序现在返回一个 new Promise,只有在 setTimeout 中的 resolve(githubUser) (**) 被调用后才会变为 settled。链中的下一个 .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);
  });
}

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