原文链接:http://javascript.info/async-await,translate with ❤️ by zhangbao.
JavaScript 提供了一种更加优雅地操作 Promise 对象的语法,称为“async
/await
”,是相当容易理解和使用的。
异步函数
我们从 async
关键字说起。它可以以下面的方式,替换之前的函数:
async function f() {
return 1;
}
函数之前的“async
”的意思很简单:函数总是返回一个 Promise 对象。如果代码中出现 return <non-prpmise>
的地方,那么 JavaScript 会自动将这个值包装起来,返回一个 resolved 状态的 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 对象包装成 Promise 对象返回。很简单,对吧?但不只是这样。还有另外一个关键字 await
——它只能在使用了 async
声明的函数中使用,非常酷。
await
语法:
// 只能在使用了 async 声明的函数中使用
let value = await promise;
JavaScript 碰到 await
关键字时,会等待后面的 Promise 解决(settles)并返回结果。
下面的例子 1 秒钟后随着 Promise 对象的状态变成 resolved,打印出 "done!"
这个字符串:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // 一直等到 promise resolved 了 (*)
alert(result); // "done!"
}
f();
函数会在 (*)
处暂停,一直等到 Promise 解决,将结果赋值给 result
。所以上面的代码在 1 秒后返回“done!
”。
我们强调一下:await
关键字让 JavaScript 等待,直到 Promise 被解决,然后继续执行代码。这并不消耗任何 CPU 资源,因为引擎可以在这期间可以执行其他任务——比如执行其他脚本、处理事件等等。
这是一种比使用 promise.then
更加优雅的方式,更容易阅读和书写。
不能再普通函数中使用 await
我们尝试在非 async 函数中使用 await 会有什么结果呢?会报错:
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
如果我们忘记在函数前面加
async
,就会出错。因为await
仅支持在async
函数中使用。
我们把《Promise 链式调用》一章里的 showAvatar()
例子拿来,用 async
/await
重写:
我们要把
.then
替换成await
调用。在函数前面加上
async
,让内部支持await
关键字的使用。
async function showAvatar() {
// 读取 JSON 数据
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// 读取 github 用户信息
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// 展示头像
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// 等待 3 秒
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
更加干净和容易阅读了,对吧?比以前好多了。
await 在顶层代码上无效
刚开始使用的人往往会忘记这一点,但是我们不能在顶级代码中使用
await
,这属于无效写法。
// 在顶层代码里使用,会发生语法错误
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
因此,我们需要为等待的代码提供一个包装的异步函数,就像上面的例子一样。
await 接受 thenable 对象
与
promise.then
类似,await
支持与 thenable 对象(就是包含可调用的 then 方法的对象)配合使用。再一次,我们的想法是,一个第三方的对象可能不是一个 Promise,而是与之兼容的:如果支持.then
,就可以用await
。例如,这里的
await
接受new Thenable(1)
:```javascript class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // function() { native code } // 1000ms 之后,使用 this.num2 作为参数调用 resolve 函数 setTimeout(() => resolve(this.num 2), 1000); // (*) } };
async function f() { // 等待 1 秒, 然后 result 赋值为 2 let result = await new Thenable(1); alert(result); }
f();
>
> 如果 `await` 后来跟的是一个带有 `.then` 方法的非 Promise 对象,那么它就会使用原生提供的 `resolve`、`reject` 函数作为参数调用 `.then` 方法。`await` 会一直等到这两个中的一个被调用(也就是上面代码里标记 `(*)` 的地方),然后再处理结果。
> **⚠️异步方法**
>
> 类方法也可以是异步的,只要在前面加 `async` 关键字即可。
>
> 像这样:
>
> ```javascript
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1
含义是一样的:它确保返回值是一个 Promise,并允许在内部使用
await
。
错误处理
如果一个 Promise 正常 resolve 了,然后 await promise
就会返回结果。但是一旦 reject 了,就会抛出错误,就像那一行有一个 throw
语句。
代码:
async function f() {
await Promise.reject(new Error("Whoops!"));
}
等同于:
async function f() {
throw new Error("Whoops!");
}
实际情况中,Promise 可能需要一段时间才会 reject。因此 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
块中。我们也可以同时封装多行 await
:
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// 捕获发生在 fetch 或 response.json 中发生的错误
alert(err);
}
}
f();
如果没有使用 try..catch
,由调用异步函数 f()
产生的 reject 结果,可以在附加的 .catch()
方法中处理。
async function f() {
let response = await fetch('http://no-such-url');
}
// f() 返回了一个 rejected 状态的 Promise
f().catch(alert); // TypeError: failed to fetch // (*)
如果我们忘记在此处添加 .catch 处理方法了,就会得到一个没有处理的 Promise 错误(可以在控制台看到)。我们可以使用在《Promise 链式调用》章节里提到的全局事件处理器来处理。
⚠️async/await 和 promise.then/catch
在使用 async/await 之后,就很少需要 .then 了,因为 await 会为我们等待处理结果;并且我们使用 try..catch 替代了 .catch 方法。这通常(并非总是)更加方便。
但在代码的顶层,当处于任何异步函数外面时,语法层面上不再允许使用
await
关键字,因此在此种清情形下,我们还是使用 .then/catch 方法处理最终的返回结果(成功或失败)。就像上例的 (*) 处的展示的那样。
⚠️async/await 与 Promise.all 也能照常协作
当我们需要同时等待多个 Promise 的时候,可以将这些 Promise 式的请求包装在
Promise.all
里,并使用await
等待返回结果。
// 等待多结果的返回(以数组的形式)
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
同时等待多任务执行是一个不错的选择。
练习题
问题
一、使用 async/await 重写“rethrow”例子
下面是《Promise 链式调用》一章中的“rethrow”例子。请使用 async/await
关键字重写这个例子,替换掉之前的 .then/catch
方法。
class HttpError extends Error {
constructor(response) {
super(`${response.status} for ${response.url}`);
this.name = 'HttpError';
this.response = response;
}
}
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new HttpError(response);
}
})
}
// 询问用户名,直至输入的是一个有效的用户名
function demoGithubUser() {
let name = prompt("请输入用户名", "iliakan");
return loadJson(`https://api.github.com/users/${name}`)
.then(user => {
alert(`完整用户名: ${user.name}.`);
return user;
})
.catch(err => {
if (err instanceof HttpError && err.response.status == 404) {
alert("该用户不存在,请重新输入.");
return demoGithubUser();
} else {
throw err;
}
});
}
demoGithubUser();
二、使用 async/await 重写
还是《Promise 链式调用》一章中的例子,使用 async/await
替换 .then/catch
方法:
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new Error(response.status);
}
})
}
loadJson('no-such-user.json') // (3)
.catch(alert); // Error: 404
答案
一、使用 async/await 重写“rethrow”例子
这里没什么技巧。只要将 demoGithubUser
里的 .catch
替换为 try...catch
,并在需要的地方添加 async/await
即可:
class HttpError extends Error {
constructor(response) {
super(`${response.status} for ${response.url}`);
this.name = 'HttpError';
this.response = response;
}
}
function loadJson(url) {
let response = await fetch(url)
if (response.status == 200) {
return response.json();
} else {
throw new HttpError(response);
}
}
// 询问用户名,直至输入的是一个有效的用户名
function demoGithubUser() {
let user
while(true) {
let name = prompt("请输入用户名", "iliakan");
try {
user = await loadJson(`https://api.github.com/users/${name}`)
break // 没产生错误,退出循环
} catch(err) {
if (err instanceof HttpError && err.response.status == 404) {
alert("该用户不存在,请重新输入.");
return demoGithubUser();
} else {
throw err;
}
}
}
alert(`完整用户名: ${user.name}.`);
return user;
}
demoGithubUser();
二、使用 async/await 重写
可以在代码下查看注解:
async function loadJson(url) { // (1)
let response = await fetch(url); // (2)
if (response.status == 200) {
let json = await response.json();
return json;
} else {
throw new Error(response.status);
}
}
loadJson('no-such-user.json') // (4)
.catch(alert); // Error: 404
注解:
函数
loadUrl
成为async
的了。所有内部的
.then
替换成了await
。我们可以
return response.json()
而不是等待它,像这样:
if (response.status == 200) {
return response.json(); // (3)
}
这样的话,外部代码就要 await
直到 Promise
对象被 resolve 了。本例中,这并不重要。
- 从
loadJson
中抛出的错误被.catch
方法处理。这里,我们不能使用await loadJson(...)
,因为不是处在async
函数中。
(完)