有一种特殊的语法可以更轻松地处理 Promise,称为“async/await”。它出乎意料地易于理解和使用。
Async 函数
让我们从 async
关键字开始。它可以放在函数之前,如下所示
async function f() {
return 1;
}
函数之前的“async”一词表示一个简单的事实:函数始终返回一个 Promise。其他值会自动包装在一个已解决的 Promise 中。
例如,此函数返回一个已解决的 Promise,其结果为 1
;让我们测试一下
async function f() {
return 1;
}
f().then(alert); // 1
…我们可以显式返回一个 Promise,这将是相同的
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
因此,async
确保函数返回一个 promise,并用它包装非 promise。很简单,对吧?但不仅如此。还有另一个关键字 await
,它仅在 async
函数中起作用,而且非常酷。
Await
语法
// works only inside async functions
let value = await promise;
关键字 await
使 JavaScript 等待该 promise 结算并返回其结果。
下面是一个在 1 秒内解决的 promise 的示例
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
f();
函数执行在行 (*)
处“暂停”,并在 promise 结算时恢复,result
成为其结果。因此,上面的代码在一秒后显示“done!”。
让我们强调一下:await
会在 promise 结算之前暂停函数执行,然后使用 promise 结果恢复执行。这不会消耗任何 CPU 资源,因为 JavaScript 引擎可以在此期间执行其他作业:执行其他脚本、处理事件等。
这只是比 promise.then
更优雅的获取 promise 结果的语法。而且,它更易于阅读和编写。
await
如果我们尝试在非 async 函数中使用 await
,则会出现语法错误
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
如果忘记在函数前加上 async
,我们可能会收到此错误。如前所述,await
仅在 async
函数中起作用。
让我们从章节 Promises chaining 中获取 showAvatar()
示例,并使用 async/await
重写它
- 我们需要用
await
替换.then
调用。 - 我们还应该使函数成为
async
函数才能使它们起作用。
async function showAvatar() {
// read our JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// read github user
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// show the avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// wait 3 seconds
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
相当简洁且易于阅读,对吧?比以前好多了。
await
在现代浏览器中,当我们在模块中时,顶级 await
可以正常工作。我们将在文章 Modules, introduction 中介绍模块。
例如
// we assume this code runs at top level, inside a module
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
console.log(user);
如果我们不使用模块,或者必须支持 较旧的浏览器,则有一个通用的方法:包装到一个匿名 async 函数中。
像这样
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
await
接受“thenables”与 promise.then
类似,await
允许我们使用可 then 的对象(那些具有可调用的 then
方法的对象)。这个想法是第三方对象可能不是一个 promise,但与 promise 兼容:如果它支持 .then
,那么它足以与 await
一起使用。
这是一个 Thenable
类的演示;下面的 await
接受它的实例
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// resolve with this.num*2 after 1000ms
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// waits for 1 second, then result becomes 2
let result = await new Thenable(1);
alert(result);
}
f();
如果 await
获得一个具有 .then
的非 promise 对象,它将调用该方法,提供内置函数 resolve
和 reject
作为参数(就像它对常规 Promise
执行器所做的那样)。然后 await
等待其中一个被调用(在上面的示例中,它发生在行 (*)
中),然后继续执行结果。
要声明一个异步类方法,只需在它前面加上 async
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1 (this is the same as (result => alert(result)))
含义是相同的:它确保返回的值是一个 promise 并启用 await
。
错误处理
如果一个 promise 正常解决,那么 await promise
将返回结果。但在被拒绝的情况下,它会抛出错误,就像在该行有一个 throw
语句一样。
这段代码
async function f() {
await Promise.reject(new Error("Whoops!"));
}
…与这个相同
async function f() {
throw new Error("Whoops!");
}
在实际情况下,promise 可能需要一段时间才能拒绝。在这种情况下,在 await
抛出错误之前会有一个延迟。
我们可以使用 try..catch
捕获该错误,就像一个常规的 throw
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
在发生错误的情况下,控制会跳转到 catch
块。我们还可以包装多行
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}
f();
如果我们没有 try..catch
,那么由异步函数 f()
的调用生成的 promise 将被拒绝。我们可以附加 .catch
来处理它
async function f() {
let response = await fetch('http://no-such-url');
}
// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)
如果我们忘记在那里添加 .catch
,那么我们会收到一个未处理的 promise 错误(可以在控制台中查看)。我们可以使用全局 unhandledrejection
事件处理程序捕获此类错误,如章节 使用 promise 进行错误处理 中所述。
async/await
和 promise.then/catch
当我们使用 async/await
时,我们很少需要 .then
,因为 await
为我们处理了等待。我们可以使用常规的 try..catch
代替 .catch
。这通常(但并非总是)更方便。
但在代码的顶级,当我们位于任何 async
函数之外时,我们在语法上无法使用 await
,因此添加 .then/catch
来处理最终结果或失败错误是一种正常做法,就像上面示例的行 (*)
中一样。
async/await
与 Promise.all
配合得很好当我们需要等待多个 promise 时,我们可以将它们包装在 Promise.all
中,然后 await
// wait for the array of results
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
在发生错误的情况下,它会像往常一样从失败的 promise 传播到 Promise.all
,然后成为一个异常,我们可以使用调用周围的 try..catch
来捕获它。
总结
函数之前的 async
关键字有两个作用
- 使其始终返回一个 promise。
- 允许在其中使用
await
。
promise 前面的 await
关键字使 JavaScript 等待该 promise 结算,然后
- 如果它是一个错误,则会生成一个异常——就像在该位置调用
throw error
一样。 - 否则,它会返回结果。
它们共同提供了一个出色的框架来编写异步代码,该代码易于阅读和编写。
使用 async/await
,我们很少需要编写 promise.then/catch
,但我们仍然不应该忘记它们是基于 promise 的,因为有时(例如,在外层作用域中)我们必须使用这些方法。此外,当我们同时等待许多任务时,Promise.all
很好用。
评论
<code>
标签,对于多行代码 - 将其包装在<pre>
标签中,对于 10 行以上的代码 - 使用沙盒 (plnkr,jsbin,codepen…)