在 ECMAScript 2017 中, JavaScript 语言中新增了 async 函数和 await关键字,其本质是 promise 的语法糖,使异步代码更容易实现,同时也提升代码可阅性。让异步代码看起来更像的同步代码,所以值得学习这套新增的 API,这篇文章希望对了解 async 和 await 有所帮助。
什么是 async 和 await
async
首先来看 async 关键字,当你把 async 放在一个函数声明前面,这个函数变成了异步函数(async),有点 go 语言对协程定义感觉。那么什么又是异步函数呢(async)?
简单地说一个异步函数是一个用 async 关键字声明的函数,并且允许在其中使用 await 关键字。async 和 await关键字代表是异步的、是对 promise 行为能够以一种更简洁的风格来编写,并不需要显式声明 promise 链。
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// expected output: "resolved"
}
asyncCall();
执行上面代码会现在控制台输出 calling 然后等待 2秒后当 result 返回后才会执行 console.log(result) ,这样感觉就是好像 await 将让执行停止在这行代码直到有返回值才会向下执行console.log(result)。
function hello() { return "Hello" };
console.log(hello());
上面代码没有什么好解释的,顺序执行立即在控制台中打印出 "Hello"
试着在你的浏览器的 JS 控制台中输入以下几行。
async function hello() { return "Hello" };
console.log(hello());
let hello = async () => { return "Hello" };
hello().then((value) => console.log(value));
所当一个函数前面添加 async 关键字就表示这个函数返回一个 promise,而不是直接返回值。所以这里 hello().then((value)=> console.log(value))
await 关键字
只有把 async 和 await 关键字结合起来时,异步函数的优势才会显现出来。await 只在常规的 JavaScript 代码中的异步函数中起作用,但也可以单独用于 JavaScript 模块。
await 可以放在任何 promise 的异步函数前面,以暂停该行的代码,直到承诺实现,然后返回结果值。
你可以在调用任何返回Promise的函数时使用await,包括Web API函数。
用 async/await 重构 promise 代码
fetch('http://10.1.0.67:5000/get_image')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
})
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
到现在为止,想必你应该对什么是 promise 和 promise 的工作方式有了一些理解,接下来把用 promise 实现异步请求代码转换为用 async/await 来实现,看一看为什么 async/await 将异步编程这件事简化了。
async function myFetch() {
let response = await fetch('http://10.1.0.67:5000/get_image');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let myBlob = await response.blob();
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
}
myFetch()
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
代码变得更简单,符合我们阅读习惯从上到下顺序来读代码写逻辑,不再有到处都是的 .then() 逻辑了。
由于 async 关键字将一个函数变成一个 promise,可以重构你的代码,也可以同时使用 promise 和await 来替换原来的代码,将函数的后半部分逻辑分离出来放置在 then 代码块中,比较 flexible。
async function myFetch() {
let response = await fetch('http://10.1.0.67:5000/get_image');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.blob();
}
myFetch().then((blob) => {
let objectURL = URL.createObjectURL(blob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
}).catch(e => console.log(e));
你会注意到,把代码包裹在一个函数里面,然后给函数前加上一个关键字 async 关键字。在这个定义为异步函数中运行你的异步代码;前面所说,await 只在异步函数中执行,不然就是抛出异常。
//Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
let response = await fetch('http://10.1.0.67:5000/one');
在 myFetch() 函数的定义中,可以看到代码与之前的 promise 版本非常相似,当然也有区别。不需要在每个基于 promise 的方法的结尾加上一个.then() 然后在其中执行逻辑,而只需要在异步请求方法调用前加上一个 await 关键字,再把结果赋给一个变量。await 关键字使JavaScript 运行时在这一行暂停向下执行代码,直到异步函数调用返回其结果,随后代码便可以使用这个返回值来进一步对其进操作。
一旦异步代码,例如请求得到返回值后,代码将从下一行开始继续执行。
let response = await fetch('http://10.1.0.67:5000/get_image');
当网络请求放回 response 可用时,由将执行 fetch() promise 返回的 respone 将被分配给一个变量,解析器暂停在这一行,直至响应可用,解析器就会转到下一行,来创建一个 Blob。这一行也也是异步函数的方法,所以也就自然使用了 await。当操作的结果返回时将其从myFetch()函数中返回。
这意味着当调用myFetch()函数时,会返回一个 promise 对象,所以可以这个 promise 对象调用 .then(),来 then 方法里实现在屏幕上显示的blob。
添加对错误处理
有关错误错误处理,列出以下几种选择
- 可以使用同步的
try...catch结构和async/await,,这个例子是对我们上面展示的代码的扩展。
async function myFetch() {
try {
let response = await fetch('http://10.1.0.67:5000/get_image');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let myBlob = await response.blob();
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
} catch(e) {
console.log(e);
}
}
myFetch();
catch() {} 捕捉传递给一个错误对象 e,可以将错误信息在控制台上输出,通过 e 我们将获取到详细的错误信息,从中也会获取代码中错误被抛出的位置。
想使用我们上面展示的第 2 个(重构)版本的代码,也就是在其中混用了.then() 的方式情况下,在末尾通过.catch()来捕捉错误信息。