async/await

作者:高天阳

邮箱:13683265113@163.com

更改历史

* 2019-09-19        高天阳     初始化文档

简介

什么是async/await

async [ə'zɪŋk]:这个单词看起来很怪异,它的原型是asynchrony,意为异步。

可能有的人看了这个词想到了放在script标签里的异步脚本,但是此async非彼async, 这个async是ES2017出来的,也是用来处理异步的,和ES6中的promise类似。

async与Promise、generator有什么差别

nodeJs里面有一个典型的异步操作,下面用三种异步处理方式来读取文件readFile():

promise来读取文件

// promise.js
const fs = require("fs");
const read = function(fileName){
    return new Promise((resolve,reject)=>{
        fs.readFile(fileName,(err,data)=>{
            if (err) {
                reject(err);
            } else{
                resolve(data);
            }
        });
    });
};
read('1.txt').then(res=>{
    console.log(res.toString());
    return read('2.txt');  // 返回新的数据,然后输出
}).then(res => {
    console.log(res.toString());
    return read('3.txt');
}).then(res => {
    console.log(res.toString());
});

然后用node来运行该文件,打开命令行(win+r):

因为读取多个文件一般都会作为一个异步来处理,这样就不会阻塞程序的运行, 把fs封装成一个Promise对象,然后在下面返回数据输出,例子中的TXT文件可以写自己的数据

generator函数读取文件

// generator.js
const fs = require("fs");
const read = function(fileName){
    return new Promise((resolve,reject)=>{
        fs.readFile(fileName,(err,data)=>{
            if (err) {
                reject(err);
            } else{
                resolve(data);
            }
        });
    });
};
function * show(){
    yield read('1.txt');
    yield read('2.txt');
    yield read('3.txt');
}
const s = show();
s.next().value.then(res => {
    console.log(res.toString());
    return s.next().value;
}).then(res => {
    console.log(res.toString());
    return s.next().value;
}).then(res => {
    console.log(res.toString());
});

依然用node运行即可,这种方式代码量又高了不少,和Promise方式特别像, 只不过是把读取文件的信息放在了外面,在下面依次手动调用,特别麻烦.

async函数读取文件

const fs = require("fs");
const read = function(fileName){
    return new Promise((resolve,reject)=>{
        fs.readFile(fileName,(err,data)=>{
            if (err) {
                reject(err);
            } else{
                resolve(data);
            }
        });
    });
};
async function readByAsync(){
    let a1 = await read('1.txt');
    let a2 = await read('2.txt');
    let a3 = await read('3.txt');
    console.log(a1.toString());
    console.log(a2.toString());
    console.log(a3.toString());
}
readByAsync();

这个函数和generator函数有些类似,从例子中可以看得出来,async函数在function前面有个async作为标识, 意思就是异步函数,里面有个await搭配使用,每到await的地方就是程序需要等待执行后面的程序,语义化很强.

特点

  • 语义化强

  • 里面的await只能在async函数中使用

  • await后面的语句可以是Promise对象、数字、字符串等

  • async函数返回的是一个Promise对象

  • await语句后的Promise对象变成reject状态时,那么整个async函数会中断,后面的程序不会继续执行

基于上面的async的特点,我们会用到异常捕获机制,学过java的都知道,java中有异常捕获try...catch...

什么是try...catch...,下面让我们来看一下它的概念。

try/catch/finally 语句用于处理代码中可能出现的错误信息。 错误可能是语法错误,通常是程序员造成的编码错误或错别字。 也可能是拼写错误或语言中缺少的功能(可能由于浏览器差异)。 try语句允许我们定义在执行时进行错误测试的代码块。 catch 语句允许我们定义当 try 代码块发生错误时,所执行的代码块。 finally 语句在 try 和 catch 之后无论有无异常都会执行。 注意: catch 和 finally 语句都是可选的,但你在使用 try 语句时必须至少使用一个。 提示: 当错误发生时, JavaScript 会停止执行,并生成一个错误信息。 使用 throw 语句来创建自定义消息(抛出异常)。如果你将 throwtrycatch一起使用, 就可以控制程序输出的错误信息。

知道了这个东西是干什么的,那么我们在async中怎么用呢?

const fs = require("fs");
const read = function(fileName){
    return new Promise((resolve,reject)=>{
        fs.readFile(fileName,(err,data)=>{
            if (err) {
                reject(err);
            } else{
                resolve(data);
            }
        });
    });
};
async function readByAsync(){
    try{
        let a1 = await read('1.txt');
        let a2 = await read('2.txt');
        let a3 = await read('3.txt');
    }catch(e){
        //TODO handle the exception
    }
    console.log(a1);
    console.log(a2);
    console.log(a3);
}
readByAsync();

大家看完了这个async是不是感觉还挺有用的啊,以后工作中async就会替代generator,原理是Promise,所以说特别好用。

示例

async的基础用法

async作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。写一个async函数

async function timeout() {
  return 'hello world';
}

async的调用

语法很简单,就是在函数前面加上async关键字,来表示它是异步的,那怎么调用呢? async函数也是函数,平时我们怎么使用函数就怎么使用它, 直接加括号调用就可以了,为了表示它没有阻塞它后面代码的执行,我们在async 函数调用之后加一句console.log;

async function timeout() {
    return 'hello world'
}
timeout();
console.log('虽然在后面,但是我先执行');

打开浏览器控制台,我们看到了

async函数timeout调用了,但是没有任何输出,它不是应该返回'hello world',先不要着急, 看一看timeout()执行返回了什么?把上面的timeout()语句改为console.log(timeout())

async function timeout() {
    return 'hello world'
}
console.log(timeout());
console.log('虽然在后面,但是我先执行');

继续看控制台

原来async函数返回的是一个promise对象,如果要获取到promise返回值,我们应该用then方法,继续修改代码

async function timeout() {
    return 'hello world'
}
timeout().then(result => {
    console.log(result);
})
console.log('虽然在后面,但是我先执行');

看控制台

我们获取到了"hello world', 同时timeout的执行也没有阻塞后面代码的执行,和我们刚才说的一致。

async的回调

这时,你可能注意到控制台中的Promise有一个resolved,这是async函数内部的实现原理。 如果async函数中有返回一个值,当调用该函数时, 内部会调用Promise.solve()方法把它转化成一个promise对象作为返回, 但如果timeout函数内部抛出错误呢?那么就会调用Promise.reject()返回一个promise对象, 这时修改一下timeout函数

async function timeout(flag) {
    if (flag) {
        return 'hello world'
    } else {
        throw 'my god, failure'
    }
}
console.log(timeout(true))  // 调用Promise.resolve() 返回promise 对象。
console.log(timeout(false)); // 调用Promise.reject() 返回promise 对象。

控制台如下:

如果函数内部抛出错误, promise 对象有一个catch 方法进行捕获。

timeout(false).catch(err => {
    console.log(err)
})

await的使用

async关键字差不多了,我们再来考虑await关键字,await是等待的意思, 那么它等待什么呢,它后面跟着什么呢?其实它后面可以放任何表达式, 不过我们更多的是放一个返回promise 对象的表达式。注意await关键字只能放到async函数里面

现在写一个函数,让它返回promise对象,该函数的作用是2s之后让数值乘以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, await表示等一下,代码就暂停到这里,不再向下执行了,它等什么呢? 等后面的promise对象执行完毕,然后拿到promise resolve的值并进行返回, 返回值拿到之后,它继续向下执行。具体到 我们的代码, 遇到await之后, 代码就暂停执行了,等待doubleAfter2seconds(30) 执行完毕, doubleAfter2seconds(30) 返回的promise 开始执行,2秒之后, promise resolve了,并返回了值为60, 这时await 才拿到返回值60, 然后赋值给result,暂停结束,代码才开始继续执行,执行console.log语句。

就这一个函数,我们可能看不出async/await的作用,如果我们要计算3个数的值,然后把得到的值进行输出呢?

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, 我们可以看到,写异步代码就像写同步代码一样了,再也没有回调地域了。

最佳实践

再写一个真实的例子,我原来做过一个小功能,话费充值,当用户输入电话号码后, 先查找这个电话号码所在的省和市,然后再根据省和市,找到可能充值的面值,进行展示。

为了模拟一下后端接口,我们新建一个node 项目。 新建一个文件夹 async, 然后npm init -y 新建package.json文件,npm install express --save 安装后端依赖, 再新建server.js 文件作为服务端代码, public文件夹作为静态文件的放置位置, 在public 文件夹里面放index.html 文件, 整个目录如下

参考资料

Last updated