前言
async functions
和await
关键字是最近添加到JavaScript语言里面的。它们是ECMAScript
JavaScript版的一部分。简单来说,它们是基于promises的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码,因此它们非常值得学习。
async/await 基础
在代码中使用 async / await 有两个部分。
1. async 关键字
首先,我们使用async
关键字,把它放在函数声明之前,使其成为async function
。异步函数是一个知道怎样使用await
关键字调用异步代码的函数。
尝试在浏览器的JS控制台中键入以下行:
function hello() {return "Hello" };hello();
该函数返回“Hello” —— 没什么特别的,对吧?
如果我们将其变成异步函数呢?请尝试以下方法:
async function hello() {return "Hello" };hello();
哈。现在调用该函数会返回一个 promise。这是异步函数的特征之一 —— 它保证函数的返回值为 promise。
你也可以创建一个异步函数表达式async function expression
,如下所示:
let hello = async function() {return "Hello" };hello();
你可以使用箭头函数:
let hello = async () => {return "Hello" };
这些都基本上是一样的。
要实际使用promise完成时返回的值,我们可以使用.then()块,因为它返回的是 promise:
hello().then((value) => console.log(value))
甚至只是简写如
hello().then(console.log)
将async
关键字加到函数申明中,可以告诉它们返回的是 promise,而不是直接返回值。此外,它避免了同步函数为支持使用await
带来的任何潜在开销。在函数声明为async
时,JavaScript引擎会添加必要的处理,以优化你的程序。爽!
2. await 关键字
当await
关键字与异步函数一起使用时,它的真正优势就变得明显了 ——事实上,await
只在异步函数里面才起作用。
await
关键字可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。
你可以在调用任何返回Promise的函数时使用 await,包括Web API函数。
下面一个简单的示例 🌰:
async function hello() {return greeting = await Promise.resolve("Hello");};hello().then(alert);
当然,上面的示例不是很有用,但它确实展示了语法。
async/await 如何运作
async 函数可能包含0个或者多个await
表达式。await表达式会暂停整个async函数的执行进程并出让其控制权,只有当其等待的基于promise的异步操作被兑现或被拒绝之后才会恢复进程。promise的解决值会被当作该await表达式的返回值。使用async
/await
关键字就可以在异步代码中使用普通的try
/catch
代码块。
await
关键字只在async函数内有效。如果你在async函数体之外使用它,就会抛出语法错误SyntaxError
。
async函数一定会返回一个promise对象。如果一个async函数的返回值看起来不是promise,那么它将会被隐式地包装在一个promise中。
例如,如下代码:
async function foo() {return 1}
等价于:
function foo() {return Promise.resolve(1)}
async函数的函数体可以被看作是由0个或者多个await表达式分割开来的。从第一行代码直到(并包括)第一个await表达式(如果有的话)都是同步运行的。这样的话,一个不含await表达式的async函数是会同步运行的。然而,如果函数体内有一个await表达式,async函数就一定会异步执行。
例如:
async function foo() {await 1}
等价于
function foo() {return Promise.resolve(1).then(() => undefined)}
在await表达式之后的代码可以被认为是存在在链式调用的then回调中,多个await表达式都将加入链式调用的then回调中,返回值将作为最后一个then回调的返回值。
在接下来的例子中,我们将使用await执行两次promise,整个foo
函数的执行将会被分为三个阶段。
foo
函数的第一行将会同步执行,await将会等待promise的结束。然后暂停通过foo
的进程,并将控制权交还给调用foo
的函数。一段时间后,当第一个promise完结的时候,控制权将重新回到foo函数内。示例中将会将1
(promise状态为fulfilled)作为结果返回给await表达式的左边即result1
。接下来函数会继续进行,到达第二个await区域,此时foo
函数的进程将再次被暂停。一段时间后,同样当第二个promise完结的时候,result2
将被赋值为2
,之后函数将会正常同步执行,将默认返回undefined
。
async function foo() {const result1 = await new Promise((resolve) => setTimeout(() => resolve('1')))const result2 = await new Promise((resolve) => setTimeout(() => resolve('2')))}foo()
注意:promise链不是一次就构建好的,相反,promise链是分阶段构造的,因此在处理异步函数时必须注意对错误函数的处理。
例如,在下面的代码中,在promise链上配置了.catch
处理程序,将抛出未处理的promise错误。这是因为p2
返回的结果不会被await处理。
async function foo() {const p1 = new Promise((resolve) => setTimeout(() => resolve('1'), 1000))const p2 = new Promise((_,reject) => setTimeout(() => reject('2'), 500))const results = [await p1, await p2] // 不推荐使用这种方式,请使用 Promise.all或者Promise.allSettled }foo().catch(() => {}) // 捕捉所有的错误...
使用 async/await 重写 promise 代码
我们看一个简单的fetch
示例:
fetch('coffee.jpg').then(response => 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(e.message);});
到现在为止,你应该对 promises 及其工作方式有一个较好的理解。让我们将其转换为使用async / await
看看它使事情变得简单了多少:
async function myFetch() {let response = await fetch('coffee.jpg');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(e.message);});
它使代码简单多了,更容易理解 —— 去除了到处都是.then()
代码块!
由于async
关键字将函数转换为 promise,你可以重构以上代码 —— 使用 promise 和await
的混合方式,将函数的后半部分抽取到新代码块中。这样做可以更灵活:
async function myFetch() {let response = await fetch('coffee.jpg');return await response.blob();}myFetch().then((blob) => {let objectURL = URL.createObjectURL(blob);let image = document.createElement('img');image.src = objectURL;document.body.appendChild(image);});
1. 它是如何工作的
你会注意到我们已经将代码封装在函数中,并且我们在 function 关键字之前包含了async
关键字。这是必要的 ––你必须创建一个异步函数来定义一个代码块,在其中运行异步代码; await 只能在异步函数内部工作。
在myFetch()
函数定义中,你可以看到代码与先前的 promise 版本非常相似,但存在一些差异。不需要附加.then()
代码块到每个promise-based方法的结尾,你只需要在方法调用前添加await
关键字,然后把结果赋给变量。await
关键字使JavaScript运行时暂停于此行,允许其他代码在此期间执行,直到异步函数调用返回其结果。一旦完成,你的代码将继续从下一行开始执行。例如:
let response = await fetch('coffee.jpg');
解析器会在此行上暂停,直到当服务器返回的响应变得可用时。此时 fetch() 返回的 promise 将会完成(fullfilled),返回的 response 会被赋值给 response 变量。一旦服务器返回的响应可用,解析器就会移动到下一行,从而创建一个Blob
(后面会介绍这个blog是个什么东西)。Blob
这行也调用基于异步promise的方法,因此我们也在此处使用await
。当操作结果返回时,我们将它从myFetch()
函数中返回。
这意味着当我们调用myFetch()函数时,它会返回一个promise,因此我们可以将.then()
链接到它的末尾,在其中我们处理显示在屏幕上的blob
。
你可能已经觉得“这真的很酷!”,你是对的 —— 用更少的.then()块来封装代码,同时它看起来很像同步代码,所以它非常直观。
◾️ Blob
Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。
Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
要从其他非blob对象和数据构造一个 Blob,请使用 Blob() 构造函数。要创建一个 blob 数据的子集 blob,请使用 slice() 方法。要获取用户文件系统上的文件对应的 Blob 对象,请参阅 File 文档。
接受 Blob 对象的API也被列在 File 文档中。
2. 添加错误处理
如果你想添加错误处理,你有几个选择。
你可以将同步的try...catch
结构和async/await
一起使用 。此示例扩展了我们上面展示的第一个版本代码:
async function myFetch() {try {let response = await fetch('coffee.jpg');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
; 我们现在可以将其记录到控制台,它将向我们提供详细的错误消息,显示错误被抛出的代码中的位置。
如果你想使用我们上面展示的第二个(重构)代码版本,你最好继续混合方式并将.catch()
块链接到.then()
调用的末尾,就像这样:
async function myFetch() {let response = await fetch('coffee.jpg');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));
这是因为.catch()
块将捕获来自异步函数调用和promise链中的错误。如果你在此处使用了try/catch
代码块,则在调用myFetch()
函数时,你仍可能会收到未处理的错误。
等待 Promise.all()
async / await
建立在 promises 之上,因此它与promises提供的所有功能兼容。这包括Promise.all()
–– 你完全可以通过调用await Promise.all()
将所有结果返回到变量中,就像同步代码一样。
async function fetchAndDecode(url, type) {let response = await fetch(url);let content;if(type === 'blob') {content = await response.blob();} else if(type === 'text') {content = await response.text();}return content;}async function displayContent() {let coffee = fetchAndDecode('coffee.jpg', 'blob');let tea = fetchAndDecode('tea.jpg', 'blob');let description = fetchAndDecode('description.txt', 'text');let values = await Promise.all([coffee, tea, description]);let objectURL1 = URL.createObjectURL(values[0]);let objectURL2 = URL.createObjectURL(values[1]);let descText = values[2];let image1 = document.createElement('img');let image2 = document.createElement('img');image1.src = objectURL1;image2.src = objectURL2;document.body.appendChild(image1);document.body.appendChild(image2);let para = document.createElement('p');para.textContent = descText;document.body.appendChild(para);}displayContent().catch((e) =>console.log(e));
请看Promise.all()
行:
let values = await Promise.all([coffee, tea, description]);
在这里,通过使用await
,我们能够在三个promise的结果都可用的时候,放入values
数组中。这看起来非常像同步代码。我们需要将所有代码封装在一个新的异步函数displayContent()
中,尽管没有减少很多代码,但能够将大部分代码从 .then() 代码块移出,使代码得到了简化,更易读。
为了错误处理,我们在displayContent()
调用中包含了一个.catch()
代码块;这将处理两个函数中出现的错误。
注意: 也可以在异步函数中使用同步 finally 代码块代替 .finally() 异步代码块,以显示操作如何进行的最终报告。
async/await 的缺陷与解决方案
了解Async/await
是非常有用的,但还有一些缺点需要考虑。
Async/await
让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。 await 关键字会阻塞其后的代码,直到promise完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但你自己的代码被阻塞。
这意味着你的代码可能会因为大量await
的promises相继发生而变慢。每个await都会等待前一个完成,而你实际想要的是所有的这些promises同时开始处理(就像我们没有使用async/await
时那样)。
有一种模式可以缓解这个问题——通过将 Promise 对象存储在变量中来同时开始它们,然后等待它们全部执行完毕。让我们看一些证明这个概念的例子。
我们有两个可用的例子 ——slow-async-await.html
和fast-async-await.html
。
◾️ slow async/await
<!DOCTYPE html><html><head><meta charset="utf-8"><title>Demonstration of slow async/await</title></head><body><script>// Define custom promise functionfunction timeoutPromise(interval) {return new Promise((resolve, reject) => {setTimeout(function(){resolve("done");}, interval);});};async function timeTest() {await timeoutPromise(3000);await timeoutPromise(3000);await timeoutPromise(3000);}let startTime = Date.now();timeTest().then(() => {let finishTime = Date.now();let timeTaken = finishTime - startTime;alert("Time taken in milliseconds: " + timeTaken);})</script></body></html>
◾️ fast async/await
<!DOCTYPE html><html><head><meta charset="utf-8"><title>Demonstration of fast async/await</title></head><body><script>// Define custom promise functionfunction timeoutPromise(interval) {return new Promise((resolve, reject) => {setTimeout(function(){resolve("done");}, interval);});};async function timeTest() {const timeoutPromise1 = timeoutPromise(3000);const timeoutPromise2 = timeoutPromise(3000);const timeoutPromise3 = timeoutPromise(3000);await timeoutPromise1;await timeoutPromise2;await timeoutPromise3;}let startTime = Date.now();timeTest().then(() => {let finishTime = Date.now();let timeTaken = finishTime - startTime;alert("Time taken in milliseconds: " + timeTaken);})</script></body></html>
它们都以自定义promise函数开始,该函数使用setTimeout()
调用伪造异步进程:
function timeoutPromise(interval) {return new Promise((resolve, reject) => {setTimeout(function(){resolve("done");}, interval);});};
然后每个包含一个timeTest()
异步函数,等待三个timeoutPromise()
调用:
async function timeTest() {...}
每一个都以记录开始时间结束,查看timeTest() promise
需要多长时间才能完成,然后记录结束时间并报告操作总共需要多长时间:
let startTime = Date.now();timeTest().then(() => {let finishTime = Date.now();let timeTaken = finishTime - startTime;alert("Time taken in milliseconds: " + timeTaken);})
timeTest()
函数在每种情况下都不同。
◾️在slow-async-await.html
示例中,timeTest()
如下所示:
async function timeTest() {await timeoutPromise(3000);await timeoutPromise(3000);await timeoutPromise(3000);}
在这里,我们直接等待所有三个timeoutPromise()
调用,使每个调用3秒钟。后续的每一个都被迫等到最后一个完成 - 如果你运行第一个例子,你会看到弹出框报告的总运行时间大约为9秒。
◾️在fast-async-await.html
示例中,timeTest()
如下所示:
async function timeTest() {const timeoutPromise1 = timeoutPromise(3000);const timeoutPromise2 = timeoutPromise(3000);const timeoutPromise3 = timeoutPromise(3000);await timeoutPromise1;await timeoutPromise2;await timeoutPromise3;}
在这里,我们将三个Promise对象存储在变量中,这样可以同时启动它们关联的进程。
接下来,我们等待他们的结果 - 因为promise都在基本上同时开始处理,promise将同时完成;当你运行第二个示例时,你将看到弹出框报告总运行时间仅超过3秒!
你必须仔细测试你的代码,并在性能开始受损时牢记这一点。
另一个小小的不便是你必须将等待执行的promise封装在异步函数中。
Async/await 的类方法
最后值得一提的是,我们可以在类/对象方法前面添加async
,以使它们返回promises,并await
它们内部的promises。
class Person {constructor(first, last, age, gender, interests) {this.name = {first,last};this.age = age;this.gender = gender;this.interests = interests;}async greeting() {return await Promise.resolve(`Hi! I'm ${this.name.first}`);};farewell() {console.log(`${this.name.first} has left the building. Bye for now!`);};}let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);
第一个实例方法可以使用如下:
han.greeting().then(console.log);
浏览器的支持
决定是否使用async/await
时的一个考虑因素是支持旧浏览器。它们适用于大多数浏览器的现代版本,与promise相同; 主要的支持问题存在于Internet Explorer和Opera Mini。
如果你想使用async/await
但是担心旧的浏览器支持,你可以考虑使用BabelJS
库 —— 这允许你使用最新的JavaScript编写应用程序,让Babel找出用户浏览器需要的更改。在遇到不支持async/await
的浏览器时,Babel
的polyfill
可以自动提供适用于旧版浏览器的实现。
总结
async/await
提供了一种很好的,简化的方法来编写更易于阅读和维护的异步代码。即使浏览器支持在撰写本文时比其他异步代码机制更受限制,但无论是现在还是将来,都值得学习和考虑使用。