用 async/await 来处理异步
async作为一个关键字放到函数前面,
async function timeout() {}
主要是为了在函数里面使用await关键字。await是等待的意思,async函数执行,遇到await就会暂停执行,等待await后面的内容执行完,再向下执行。await后面,通常跟Promise对象,Promise对象resolve或reject,就表示await后面的内容执行完了,函数继续执行。继续执行,是执行await 表达式,await 和后面的内容,整体是一个表达式,表达式是有返回值的,因此await 表达式可以赋值给一个变量。如果等待的Promise对象resolve了,await 整个表达式的值,就是resolve的值, 赋值给一个变量,就可以获取到Promise resolve的结果。如果等待的Promise对象reject了, await表达式,也会抛出错误,错误就是reject的值。写一个函数,返回promise 对象,2s 之后resovle,让数值乘以2
// 2s 之后返回双倍的值
function doubleAfter2seconds(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2 * num)
}, 2000);
} )
}
再写一个async/await 函数, 由于await 后面跟promise对象,所以它后面可以写 doubleAfter2seconds 函数的调用
async function testResult() {
let result = await doubleAfter2seconds(30);
console.log(result);
}
现在调用testResult 函数
testResult(); // 2s 之后,输出了60
testResult执行,遇到await就暂停了, 等待doubleAfter2seconds(30) 执行完毕,doubleAfter2seconds(30) 返回的promise 开始执行,2秒 之后,promise resolve 了, testResult 函数继续执行,执行await 表达式,由于Promise resovle了,await 整个表达式的值是60, 赋值给result,执行 console.log语句。当然,async函数中可以有多个await表达式,
async function testResult() {
let first = await doubleAfter2seconds(30);
let second = await doubleAfter2seconds(50);
let third = await doubleAfter2seconds(30);
console.log(first + second + third); // 6秒后,控制台输出220
}
稍微改写一个doubleAfter2seconds, 让它2s之后rejecrt,看一下 Promise reject的执行情况
function doubleAfter2seconds(num) { return new Promise((resolve, reject) => { setTimeout(() => { reject('乘积出错了') }, 2000); }) }
testResult执行, 遇到await, 暂停执行,但2s后,Promise reject了,整个await表达式,抛出错误,由于testResult 没有捕获错误,错误继续向上抛,到了控制台,Uncaught (in promise) 乘积出错了,整个程序终止执行。由于await是一个表达式,像2/0 一样,因此可以使用try catch 进行错误捕获。在async函数,把所有await表达式,用try 包括起来,
async function testResult() { try { let first = await doubleAfter2seconds(30); let second = await doubleAfter2seconds(50); let third = await doubleAfter2seconds(30); } catch (error) { console.log(error) // 控制台输出 乘积出错了。 } }
try catch中,catch 的错误,就是await 的promise reject的错误。大部分情况下,async和await会同时使用。但async 也可以单独使用,内部没有await语句,
async function timeout() { for (let index = 0; index < 3; index++) { console.log( index); } return 'hello world' } console.log(timeout());
此时,async函数变成了同步函数,从上到下执行完结束,只不过它的返回值是一个promise,如果函数内部返回普通值,会调用Promise.solve() 把它转化成一个promise 对象返回, 如果函数内部抛出错误,就会调用Promise.reject() 转化成一个promise 对象返回
async function timeout() { throw new Error('rejected'); } console.log(timeout());
想要获取到async 函数的执行结果,就要调用promise的then 或catch 来给它注册回调函数,
async function timeout() { return 'hello world' } timeout().then(result => { console.log(result); })
async function testResult() { let first = await doubleAfter2seconds(30); let second = await doubleAfter2seconds(50); console.log(first + second); } testResult(); console.log('先执行');
当async 函数testResult执行时,会以同步的方法,从上到下依次执行,直到遇到await, testResult会暂停执行,程序控制权会回到 testResult调用的地方,后面的代码执行,console.log执行。由于是主程序执行完毕,开启事件循环。2s后,第一个doubleAfter2seconds resolve了,程序的控制权回到testResult中,resolve的结果赋值给first,继续向下执行,遇到await,程序的控制权又回到testResult的地方。2s后,doubleAfter2seconds resolve了,程序的控制权回到testResult中,resolve的结果赋值给second,然后执行conosle.log。async和await 相当于结合了generator 和 promise, await后面的代码,可以看作是await 的promise的then 的回调函数中要执行的内容。
写一个真实的例子,话费充值,当用户输入电话号码后,先查找这个电话号码所在的省和市,然后再根据省和市,找到可能充值的面值,进行展示。为了模拟一下后端接口,新建一个node 项目。 新建一个文件夹 async, 然后npm init -y 新建package.json文件,npm install express,再新建server.js 文件作为服务端代码, public文件夹作为静态文件的放置位置, 在public 文件夹里面放index.html 文件, 整个目录如下
server.js 文件如下,建立最简单的web 服务器
const express = require('express'); const app = express();// express.static 提供静态文件,就是html, css, js 文件 app.use(express.static('public')); app.listen(3000, () => { console.log('server start'); })
再写index.html 文件,我在这里用了vue构建页面,用axios 发送ajax请求, 为了简单,用cdn 引入它们。 html部分很简单,一个输入框,让用户输入手机号,一个充值金额的展示区域, js部分,按照vue 的要求搭建了模版
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Async/await</title> <!-- CDN 引入vue 和 axios --> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> </head> <body> <div id="app"> <!-- 输入框区域 --> <div style="height:50px"> <input type="text" placeholder="请输入电话号码" v-model="phoneNum"> <button @click="getFaceResult">确定</button> </div> <!-- 充值面值 显示区域 --> <div> 充值面值: <span v-for="item in faceList" :key='item'> {{item}} </span> </div> </div> <!-- js 代码区域 --> <script> new Vue({ el: '#app', data: { phoneNum: '12345', faceList: ["20元", "30元", "50元"] }, methods: { getFaceResult() { } } }) </script> </body> </html>
为了得到用户输入的手机号,给input 输入框添加v-model指令,绑定phoneNum变量。展示区域则是 绑定到faceList 数组,v-for 指令进行展示, 这时命令行nodemon server 启动服务器,如果你没有安装nodemon, 可以npm install -g nodemon 安装它。启动成功后,在浏览器中输入 http://localhost:3000, 可以看到页面如下, 展示正确
现在我们来动态获取充值面值。当点击确定按钮时, 我们首先要根据手机号得到省和市,所以写一个方法来发送请求获取省和市,方法命名为getLocation, 接受一个参数phoneNum , 后台接口名为phoneLocation,当获取到城市位置以后,我们再发送请求获取充值面值,所以还要再写一个方法getFaceList, 它接受两个参数, province 和city, 后台接口为faceList,在methods 下面添加这两个方法getLocation, getFaceList
methods: { //获取到城市信息 getLocation(phoneNum) { return axios.post('phoneLocation', { phoneNum }) }, // 获取面值 getFaceList(province, city) { return axios.post('/faceList', { province, city }) }, // 点击确定按钮时,获取面值列表 getFaceResult () { } }
现在再把两个后台接口写好,为了演示,写的非常简单,没有进行任何的验证,只是返回前端所需要的数据。Express 写这种简单的接口还是非常方便的,在app.use 和app.listen 之间添加如下代码
// 电话号码返回省和市,为了模拟延迟,使用了setTimeout app.post('/phoneLocation', (req, res) => { setTimeout(() => { res.json({ success: true, obj: { province: '广东', city: '深圳' } }) }, 1000); }) // 返回面值列表 app.post('/faceList', (req, res) => { setTimeout(() => { res.json( { success: true, obj:['20元', '30元', '50元'] } ) }, 1000); })
最后是前端页面中的click 事件的getFaceResult, 由于axios 返回的是promise 对象,我们使用then 的链式写法,先调用getLocation方法,在其then方法中获取省和市,然后再在里面调用getFaceList,再在getFaceList 的then方法获取面值列表,
// 点击确定按钮时,获取面值列表 getFaceResult () { this.getLocation(this.phoneNum) .then(res => { if (res.status === 200 && res.data.success) { let province = res.data.obj.province; let city = res.data.obj.city; this.getFaceList(province, city) .then(res => { if(res.status === 200 && res.data.success) { this.faceList = res.data.obj } }) } }) .catch(err => { console.log(err) }) }
现在点击确定按钮,可以看到页面中输出了 从后台返回的面值列表。这时你看到了then 的链式写法,有一点回调地域的感觉。现在我们在有async/ await 来改造一下。
首先把 getFaceResult 转化成一个async 函数,就是在其前面加async, 因为它的调用方法和普通函数的调用方法是一致,所以没有什么问题。然后就把 getLocation 和
// 点击确定按钮时,获取面值列表 async getFaceResult () { let location = await this.getLocation(this.phoneNum); if (location.data.success) { let province = location.data.obj.province; let city = location.data.obj.city; let result = await this.getFaceList(province, city); if (result.data.success) { this.faceList = result.data.obj; } } }
现在代码的书写方式,就像写同步代码一样,没有回调的感觉,非常舒服。
现在就还差一点需要说明,那就是怎么处理异常,如果请求发生异常,怎么处理? 它用的是try/catch 来捕获异常,把await 放到 try 中进行执行,如有异常,就使用catch 进行处理。
async getFaceResult () { try { let location = await this.getLocation(this.phoneNum); if (location.data.success) { let province = location.data.obj.province; let city = location.data.obj.city; let result = await this.getFaceList(province, city); if (result.data.success) { this.faceList = result.data.obj; } } } catch(err) { console.log(err); } }
现在把服务器停掉,可以看到控制台中输出net Erorr,整个程序正常运行。