什么是 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的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享


















