所谓的"承诺"
可能反正我写的东西有些枯燥,但是久而久之,功力一定深厚,所以会比较小众,坚持写,写到天荒地老。坚持翻译好文。
让我们回到介绍一章中提到的问题:回调:我们有一个异步任务序列一个接一个地执行——例如,加载脚本。我们怎样才能把它写好呢?
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,则链上的其余部分将等待直到它解决。当它这样做时,它的结果(或错误)将进一步传递。
以下是全貌: