让我们回到 简介:回调 一章中提到的问题:我们有一系列异步任务需要依次执行——例如,加载脚本。我们如何才能很好地编写代码?
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
处理程序的链传递。
这里的流程是
- 初始 Promise 在 1 秒内解析
(*)
, - 然后,将调用
.then
处理程序(**)
,它又会创建一个新的 Promise(使用2
值解决)。 - 下一个
then
(***)
获取上一个的结果,对其进行处理(加倍),然后将其传递给下一个处理程序。 - …依此类推。
随着结果沿着处理程序链传递,我们可以看到一系列alert
调用:1
→ 2
→ 4
。
整个过程之所以有效,是因为每次对.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
是可以的,因为嵌套函数可以访问外部作用域。在上面的示例中,最嵌套的回调可以访问所有变量 script1
、script2
、script3
。但这是一种例外,而不是规则。
准确地说,一个处理程序可能返回的不仅仅是一个 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
的可调用方法,那么它将调用该方法,提供原生函数 resolve
、reject
作为参数(类似于执行器),并等到其中一个被调用。在上面的示例中,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,则链的其余部分将等待它解决。当它解决时,它的结果(或错误)将进一步传递。
以下是完整图片
评论
<code>
标记,对于多行 - 将其包装在<pre>
标记中,对于 10 行以上 - 使用沙盒 (plnkr、jsbin、codepen…)