用 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和await的基本用法说完了,再说一个细节。当async函数执行时,遇到await会暂停执行,JS线程会怎样,它会暂停吗?不会,async函数遇到await暂停后,会把控制权交回到async函数调用的地方, async 函数后面的内容会执行。
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 和

getFaceList 放到await 后面,等待执行, getFaceResult  函数修改如下
            // 点击确定按钮时,获取面值列表
            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,整个程序正常运行。

  

  

posted @ 2018-02-05 15:53  SamWeb  阅读(312178)  评论(36编辑  收藏  举报