一边学习前端,一边通过博客的形式自己总结一些东西,当然也希望帮助一些和我一样开始学前端的小伙伴。
如果出现错误,请在评论中指出,我也好自己纠正自己的错误
author: thomaszhou
让我们开始学习async和await
async/await使用同步的思维,来解决异步的问题。
-
async的优点
利用async创建的函数也是异步函数,就像setTimeout那种一样
async/await 的优势在于处理 then 链
:- 如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了,因为promise参数传递太麻烦了,而async/await特别方便
- async可以直接接收传递的变量,但是peomise的then是独立作用于,如果要取值,就要将部分数据暴露在最外层,在 then 内部赋值一次.
-
相比较generator
- (1)内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
- (2)更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
- (3)更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
同步和异步的理解:当我们发出了请求,并不会等待响应结果,而是会继续执行后面的代码,响应结果的处理在之后的事件循环中解决。那么同步的意思,就是等结果出来之后,代码才会继续往下执行。
我们可以用一个两人问答的场景来比喻异步与同步。A向B问了一个问题之后,不等待B的回答,接着问下一个问题,这是异步。A向B问了一个问题之后,然后就笑呵呵的等着B回答,B回答了之后他才会接着问下一个问题,这是同步。
1、安装支持
babel已经支持,所以我们可以在webpack中使用 首先在当前项目中使用npm下载babel-loader。
npm install babel-loader --save-dev复制代码
然后在配置文件webpack.confing.dev.js中配置
,在module.exports.module.rules中
添加如下配置元素即可。
{ test: /\.(js|jsx)$/, include: paths.appSrc, loader: require.resolve('babel-loader'), options: { cacheDirectory: true, }, },复制代码
如果你使用最新版本的create-react-app或者vue-cli来构建你的代码,那么它们应该已经支持了该配置。
2、普通声明和await使用
- async函数实际上返回的是一个Promise对象
async function fn() { return 30;}// 或者const fn = async () => { return 30;}复制代码
在声明函数时,前面加上关键字async,这就是async的用法。当我们用console.log打印出上面声明的函数fn,我们可以看到如下结果:
console.log(fn());//resultPromise = { __proto__: Promise, [[PromiseStatus]]: "resolved", [[PromiseValue]]: 30}复制代码
很显然,fn的运行结果其实就是一个Promise对象。因此我们也可以使用then来处理后续逻辑。
fn().then(res => { console.log(res); // 30})复制代码
await的使用-------------------
await的含义为等待。就是代码需要等待await后面的函数运行完并且有了返回结果之后,才继续执行下面的代码。这正是同步的效果。
但是我们需要注意的是,await关键字只能在async函数中使用。并且await后面的函数运行后必须返回一个Promise对象才能实现同步的效果。
当我们使用一个变量去接收await的返回值时,如:const temp = await fn();
该返回值temp为Promise中resolve出来的值(也就是PromiseValue)。
// 定义一个返回Promise对象的函数function fn() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(30); }, 1000); })}// 然后利用async/await来完成代码const foo = async () => { const t = await fn(); // 将30传入 console.log(t);}foo();console.log('begin')// begin// 30复制代码
首先我们定义了一个函数fn(),这个函数返回Promise,并且会延时 1 秒,resolve并且传入值30,foo函数在定义时使用了关键字async,然后函数体中配合使用了await,最后执行foo()。整个程序会在 1 秒后输出30,也就是说foo()中常量t取得了fn()中resolve的值,并且通过await阻塞了后面的代码执行,直到fn()这个异步函数执行完。
运行这个例子我们可以看出,当在async函数中,运行遇到await时,就会等待await后面的函数运行完毕,而不会直接执行next code。
可以看到begin优先输出,是因为async/await创建的foo()函数也是异步函数,所以你懂的
如果我们直接使用promise的then方法的话,想要达到同样的结果,就不得不把后续的逻辑写在then方法中。
const foo = () => { return fn().then(t => { console.log(t); console.log('next code'); })}foo();复制代码
很显然如果使用async/await的话,代码结构会更加简洁,逻辑也更加清晰。
从代码片段中不难看出 Promise 没有解决好的事情,比如要有很多的 then 方法,整块代码会充满 Promise 的方法,而不是业务逻辑本身.
而且每一个 then 方法内部是一个独立的作用域,要是想共享数据,就要将部分数据暴露在最外层,在 then 内部赋值一次.
虽然如此,Promise 对于异步操作的封装还是非常不错的,所以 async/await 是基于 Promise 的,await 后面是要接收一个 Promise 实例。
3、异常处理
在Promise中,我们知道是通过catch的方式来捕获异常。而当我们使用async时,则通过try/catch来捕获异常。
function fn() { return new Promise((resolve, reject) => { setTimeout(() => { reject('some error.'); }, 1000); })}const foo = async () => { try { await fn(); } catch (e) { console.log(e); // some error }}foo();复制代码
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
如果有多个await函数,那么只会返回第一个捕获到的异常。
function fn1() { return new Promise((resolve, reject) => { setTimeout(() => { reject('some error fn1.');// 设置reject }, 1000); })}function fn2() { return new Promise((resolve, reject) => { setTimeout(() => { reject('some error fn2.'); // 设置reject }, 1000); })}const foo = async () => { try { await fn1(); await fn2(); } catch (e) { console.log(e); // some error fn1. }}foo();复制代码
async
4、async/await 的优势在于处理 then 链
单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。
例子一:
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:
/** * 传入参数 n,表示这个函数执行的时间(毫秒) * 执行的结果是 n + 200,这个值将用于下一步骤 */function takeLongTime(n) { return new Promise(resolve => { setTimeout(() => resolve(n + 200), n); });}function step1(n) { console.log(`step1 with ${n}`); return takeLongTime(n);}function step2(n) { console.log(`step2 with ${n}`); return takeLongTime(n);}function step3(n) { console.log(`step3 with ${n}`); return takeLongTime(n);}复制代码
- Promise 方式来实现这三个步骤的处理
function doIt() { console.time("doIt"); const time1 = 300; step1(time1) .then(time2 => step2(time2)) .then(time3 => step3(time3)) .then(result => { console.log(`result is ${result}`); console.timeEnd("doIt"); });}doIt();复制代码
- async/await 方式来实现这三个步骤的处理
async function doIt() { console.time("doIt"); const time1 = 300; const time2 = await step1(time1); const time3 = await step2(time2); const result = await step3(time3); console.log(`result is ${result}`); console.timeEnd("doIt");}doIt();复制代码
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样
例子二:
现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。
function step1(n) { console.log(`step1 with ${n}`); return takeLongTime(n);}function step2(m, n) { console.log(`step2 with ${m} and ${n}`); return takeLongTime(m + n);}function step3(k, m, n) { console.log(`step3 with ${k}, ${m} and ${n}`); return takeLongTime(k + m + n);}复制代码
- 用 async/await 来写:
async function doIt() { console.time("doIt"); const time1 = 300; const time2 = await step1(time1); const time3 = await step2(time1, time2); const result = await step3(time1, time2, time3); console.log(`result is ${result}`); console.timeEnd("doIt");}doIt();复制代码
除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?
function doIt() { console.time("doIt"); const time1 = 300; step1(time1) .then(time2 => { return step2(time1, time2) .then(time3 => [time1, time2, time3]); }) .then(times => { const [time1, time2, time3] = times; return step3(time1, time2, time3); }) .then(result => { console.log(`result is ${result}`); console.timeEnd("doIt"); });}doIt();复制代码
有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!
5、await in for 循环
- await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。(注意想forEach,map,reduce这种也是函数!!!!)
- 正确的写法是采用 for 循环。!!!!
async function dbFuc(db) { let docs = [{}, {}, {}]; // 报错 docs.forEach(function (doc) { await db.post(doc); });}复制代码
上面代码会报错,因为 await 用在普通函数之中了。但是,如果将 forEach 方法的参数改成 async 函数,也有问题。
async function dbFuc(db) { let docs = [{}, {}, {}]; // 可能得到错误结果 docs.forEach(async function (doc) { await db.post(doc); });}复制代码
上面代码可能不会正常工作,原因是这时三个 db.post 操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。
async function dbFuc(db) { let docs = [{}, {}, {}]; for (let doc of docs) { await db.post(doc); }}复制代码
- 如果确实希望多个请求并发执行,可以使用 Promise.all 方法。
- 先将多个函数(任务)都保存到doc这个数组中,就可以保存多个任务,然后再实现并发执行
async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results);}// 或者使用下面的写法async function dbFuc(db) { let docs = [{}, {}, {}]; // 将多个函数(任务)都保存到doc这个数组中,就可以保存多个任务,然后再实现并发执行 let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results);}复制代码
6、实践
在实践中我们遇到异步场景最多的就是接口请求,那么这里就以jquery中的$.get为例简单展示一下如何配合async/await来解决这个场景。
// 先定义接口请求的方法,由于jquery封装的几个请求方法都是返回Promise实例,因此可以直接使用await函数实现同步const getUserInfo = () => $.get('xxxx/api/xx');const clickHandler = async () => { try { const resp = await getUserInfo(); // resp为接口返回内容,接下来利用它来处理对应的逻辑 console.log(resp); // do something } catch (e) { // 处理错误逻辑 }}复制代码
7、一个问题测试
题目
可修改下面的 aa() 函数,目的是在一秒后用 console.log() 输出 want-value
function aa() { setTimeout(function() { return "want-value"; }, 1000);}复制代码
- 但是,有额外要求:
- aa() 函数可以随意修改,但是不能有 console.log()
- 执行 console.log() 语句里不能有 setTimeout 包裹
解答
问题的主要目的是考察对异步调用执行结果的处理,既然是异步调用,那么不可能同步等待异步结果,结果一定是异步的
setTimeout() 经常用来模拟异步操作。最早,异步是通过回调来通知(调用)处理程序处理结果的
function aa() { return new Promise((resolve) => { setTimeout(function() { resolve('want-value'); },1000); }); }async function fn() { let temp = await aa(); console.log(temp);}fn();复制代码
参考文章: