持续创作,加速成长!这是我参与「掘金日新计划 · 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;
});
如图所示,上面的代码取到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
调用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}`));
// ...