什么是 async ?
async/await
是 ES7 的标准,Promise
是 ES6 标准,async/await
这套 API 也是用来帮助我们写异步代码的,它是构建在 Promise
之上的。
async的特点:
async
一般不单独使用,而是和await
一起使用。async
函数被调用的时候,会立即返回一个Promise
。- 当
async
函数执行到 await 的时候,会暂停整个async
函数的执行进程并出让其控制权,只有当其等待的基于Promise
的异步操作被兑现或被拒绝之后才会恢复进程。
await的特点:
await
不能单独使用,如果在非async
函数内部被调用会报错。await
后面一般跟一个Promise
,也可以是其他的,比如一个数值,或者一个变量,或者一个函数。如果await
后面不是一个Promise
就会返回一个已经resolve
的Promise
。await
相当于Promise
的then
。
为什么要使用 async ?
(一) 隐藏 Promise ,更易于理解
假设我们想请求一个接口,然后把响应的数据打印出来,并且捕获异常。用 Promise
大概是这样写:
function logFetch(url) {return fetch(url).then((response) => response.text()).then((text) => {console.log(text);}).catch((err) => {console.error("fetch failed", err);});}
如果用 async
函数来写,大概是这个样子:
async function logFetch(url) {try {const response = await fetch(url);console.log(await response.text());} catch (err) {console.log("fetch failed", err);}}
虽然代码的行数差不多,但是代码看起来更加简洁,少了很多 then
的嵌套。请求一个接口数据,然后打印,就像你看到的,很简单,可阅读性更高。
(二)用同步的思路写异步逻辑
async/await
最大的优势就是我们可以用同步的思路来写异步的业务逻辑,所以代码整体看起来更加容易看懂。
下面举个例子
我们想获取一个网络资源的大小,如果使用 Promise
大概可能是这个样子:
function getResponseSize(url) {return fetch(url).then((response) => {const reader = response.body.getReader();let total = 0;return reader.read().then(function processResult(result) {if (result.done) return total;const value = result.value;total += value.length;console.log("Received chunk", value);return reader.read().then(processResult);});});
这样就形成了最初“回调地狱",这个代码也并不是很好理解,因为中间有一个循环的过程,而且这个执行的过程的异步的,并不像我们之前学到的一个链式调用能解决的。
接下来我们看一下 async 函数是怎么处理的。
async function getResponseSize(url) {const response = await fetch(url);const reader = response.body.getReader();let result = await reader.read();let total = 0;while (!result.done) {const value = result.value;total += value.length;console.log("Received chunk", value);result = await reader.read();}return total;}
这样看起来就更加流畅了,因为 await
表达式会阻塞运行,甚至可以直接阻塞循环,所以整体看起来像同步的代码,也更符合直觉,更容易读懂这个代码。
日常妙用 await
async
与 await
不单可以使用在请求接口上,还可以为代码适当造成"阻塞"。
有时候我们就是想要一些功能函数延迟几秒后执行:
async function delayTwoFunt() {return new Promise((resolve) => {setTimeout(() => {resolve("2m,");}, 2000);});}async function firstFunt() {console.log("start");const delayRes = await delayTwoFunt();console.log("delayRes: ", delayRes);console.log("firstFunt");}firstFunt();
小心 await 阻塞
由于 await
能够阻塞 async
函数的运行,所以代码看起来更像同步的代码,更容易阅读和理解。但是要小心 await
阻塞,因为有些阻塞是不必要的,不恰当使用可能会影响代码的性能。
假如我们要把一个网络数据和本地数据合并,错误的实例可能是这样子:
async function combineData(url, file) {let networkData = await fetch(url);let fileData = await readeFile(file);console.log(networkData + fileData);}
其实我们不用等一个文件读完了,再去读下个文件,我们可以两个文件一起读,读完之后再进行合并,这样能提高代码的运行速度。我们可以这样写:
async function combineData(url, file) {let fetchPromise = fetch(url);let readFilePromise = readFile(file);let networkData = await fetchPromise;let fileData = await readFilePromise;console.log(networkData + fileData);}
这样的话,就可以同时 网络请求 和 读取文件 了,可以节省很多时间。这里主要是利用了 Promise
一旦创建就立刻执行的特点,如果你熟悉 Promise
的话,可以直接使用 Promise.all
的方式来处理,或者 await
后面跟 Promise.all
以下代码的执行顺序是?
async function foo() {console.log(1);await bar();console.log(2);}async function bar() {console.log(3);}foo();console.log(4);//1342
过程分析:
- 首先一进来是创建了两个函数的,我们先不看函数的创建位置,而是看它的调用位置
- 发现
foo
函数被调用了,然后去看看调用的内容 - 执行函数中的同步代码
log(1)
,之后碰到了await
,它会阻塞foo后面代码的执行,因此会先去执行bar
中的同步代码,然后 跳出foo
- 跳出
foo
函数后,执行同步代码log(4)
- 在一轮宏任务全部执行完之后,再来执行刚刚
await
后面的内容log(2)。
在这里,你可以理解为「紧跟着await后面的语句相当于放到了 new Promise
中,下一行及之后的语句相当于放在 Promise.then
中」。
让我们来看看将await转换为 Promise.then
的伪代码:
async function foo() {console.log(1);// 原来代码// await bar();// console.log(2);// 转换后代码new Promise((resolve) => {resolve();bar();}).then((res) => console.log(2));}async function bar() {console.log(3);}foo();console.log(4);//1342// 复制代码转换后的伪代码和前面的执行结果是一样的。
异常处理
try...catch
在 async
函数中,异常处理一般是 try...catch
,如果没有进行 try...catch
,await
表达式一旦 reject
,async
函数返回的 Promise
就会 reject
。
其实结合 Promise
来看,如果一个 Promise
状态敲定为 reject
,并且后续的 then
没有传入 reject
函数,或者没有 catch
,那么就会抛出异常。从这个角度来看,在 async
函数中用 try...catch
来包住 await
表达式,可能就是 catch
住这个异常,并且把这个 reject
信息传到 catch
里面。
这里就不举例子了。
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享