所谓的"承诺"

共 7405字,需浏览 15分钟

 ·

2021-02-18 11:11

可能反正我写的东西有些枯燥,但是久而久之,功力一定深厚,所以会比较小众,坚持写,写到天荒地老。坚持翻译好文。


让我们回到介绍一章中提到的问题:回调:我们有一个异步任务序列一个接一个地执行——例如,加载脚本。我们怎样才能把它写好呢?

Promise 提供了一些方法来做到这一点。

在本章中,我们将讨论 Promise chaining。

它是这样的:

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处理程序链传递结果。

这里的流程是:

  • 初始 Promise 在1秒内解决(*),

  • 然后 then 处理程序被调用(**)。

  • 它返回的值被传递给下一个 .then 处理程序(***)

  • 当结果沿着处理程序链传递时,我们可以看到警报调用的序列:1→2→4。

整件事都成功了,因为一个 Promise 回调。然后返回一个 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 上得到相同的结果-那个 Promise 的结果。因此,在上述代码中,所有警报显示相同的:

在实践中,我们很少需要对一个 Promise 使用多个处理程序。链接更常用。

Returning promises

在.then(handler)中使用的处理程序可以创建并返回 promise。

在这种情况下,进一步的处理程序会等待,直到它安定下来,然后得到它的结果。

例如:

new Promise(function(resolve, reject{

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

}).then(function(result{

  alert(result); // 1

  return new P
  
}).then(function(result{

  alert(result); // 4

});

在这里,第一个 .then 显示了1,并在(*)行中返回新的 Promise(…)。一秒钟后,它进行解析,结果 (resolve的参数,这里是result * 2) 被传递给第二个 .then 的处理器。该处理程序在(**)行中,它显示2并执行相同的操作。

因此输出与前面的示例相同:1→2→4,但现在警报调用之间有1秒的延迟。

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

Example: loadScript

让我们在上一章定义的promisified 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调用返回一个承诺,下一个.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 是可以的,因为嵌套函数可以访问外部作用域。在上面的例子中,嵌套最多的回调函数可以访问所有变量script1、script2、script3。但这是一个例外,而不是规律。

Thenables

确切地说,处理程序可能返回的不是一个promise,而是一个所谓的“thenable”对象——一个具有方法的任意对象。它会像承诺一样被对待。

其思想是,第三方库可以实现它们自己的“承诺兼容”对象。它们可以拥有一组扩展的方法,但也与本机承诺兼容,因为它们实现了.then。

下面是一个可执行对象的例子:

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的可调用方法,那么它调用该方法,提供本地函数resolve,拒绝作为参数(类似于executor),并等待直到其中一个被调用。在上面的例子中,resolve(2)在1秒(**)之后被调用。然后,结果将沿着链向下传递。

这个特性允许我们将自定义对象与promise链集成在一起,而不必从promise继承。

Bigger example: fetch

在前端编程中,承诺通常用于网络请求。我们来看一个扩展的例子。

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

let promise = fetch(url);

468/5000 这将向url发出一个网络请求并返回一个承诺。当远程服务器以头响应时,但在下载完整响应之前,promise将使用响应对象进行解析。

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

下面的代码向用户发出请求。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. 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处理程序现在返回新的承诺,只有在setTimeout(**)中调用resolve(githubUser)之后才会得到解决。下一个 .then 在链中等待它。

作为一种良好的实践,异步操作应该总是返回promise。这使得我们有可能在事件发生后制定行动计划;即使我们现在不打算扩展这条链,我们以后可能会需要它。

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

function loadGithubUser(name{
  return fetch(`https://api.github.com/users/${name}`)
    .then(response => response.json());
}

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}`));
  // ...

Summary

如果.then(或catch/finally,没关系)处理程序返回promise,则链上的其余部分将等待直到它解决。当它这样做时,它的结果(或错误)将进一步传递。

以下是全貌:


浏览 47
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报