使用 async/await 的异步编程

上面讲解了 Node.js 中的回调函数,但回调只适用于简单的异步场景!当程序中有很多回调时,代码会变得非常复杂,而且调试也会很麻烦,因此在 ES2015 标准中新增了 Promise 特性,用来帮助处理异步代码而不涉及使用回调。在更高级的 ES2017 标准中,又新增了 async/await 语法,使得异步编程更加简单。本节将对如何使用 async/await 实现异步编程进行讲解。

Promise 基础

async/await 建立在 Promise 之上,因此要学习 async/await,首先应该对 Promise 有所了解。

Promise 是 ES2015 标准中提供的一种处理异步代码(而不会陷入回调地狱)的方式,它本质上是一个对象,使用 new Promise() 构造函数可以创建该对象,new Promise() 构造函数中需要传入一个具有 resolvereject 参数的函数,形式如下:

var p = new Promise(function(resolve, reject){
});

其中,resolve 表示异步操作执行成功后的回调函数(其参数通常用 data 表示),reject 表示异步操作执行失败后的回调函数(其参数通常用 err 表示)。Promise 对象共有 3 种状态。

  • pending(进行中):Promise 对象刚被创建时的状态,表示异步操作还未完成。

  • fulfilled(已完成):表示异步操作已经完成,并返回了一个值。

  • rejected(已拒绝):表示异步操作失败,返回一个错误信息。

回调地狱是从英语 callback hell 翻译过来的一个名词,形容的是在异步 JavaScript 中的一种现象:回调函数写得太多了,回调嵌套回调,让人很难凭直觉看懂代码。

例如,定义一个 runAsync 函数,该函数中创建一个 Promise 对象,在程序执行 1 秒后输出一个字符串,代码如下:

function runAsync(){
     var p = new Promise(function(resolve, reject){
          setTimeout(function(){
               console.log('执行异步操作1')
               resolve('promise1')
          }, 1000)
     })
     return p
}
runAsync()

运行上面代码后,会输出 “执行异步操作1”,但其中的 resolve('promise1') 并没有执行,它的作用是什么呢?

前面我们提到 resolve 是异步操作执行成功后要执行的回调函数,那么它如何执行呢?Promise 对象提供了 then 方法,用来指定执行 resolve 回调。

例如,下面的代码使用上面创建的 Promise 对象,并在 then 方法中执行 resolve 回调:

runAsync().then(function(data){
     console.log(data)
})

运行上面代码,会输出以下结果:

执行异步操作1
promise1

从上面的示例可以看出,then 方法中的函数就类似于一个回调函数,但它能够在异步操作完成之后被执行,这就是 Promise 的好处,它能够将原来的回调函数分离出来,在异步操作执行完后,再去执行回调函数。

另外,使用 Promise 实现异步还有一个最大的特点:链式调用回调函数,即它可以在 then 方法中继续创建 Promise 对象并返回,然后继续调用 then 来进行回调操作。

例如,按照上面 runAsync 函数的方式再定义两个 runAsync2runAsync3 函数,代码如下:

runAsync()
     .then(function(data){
          console.log(data)
          return runAsync2()
     })
     .then(function(data){
          console.log(data)
          return runAsync3()
     })
     .then(function(data){
          console.log(data)
     })

运行上面代码,结果如下:

执行异步操作1
promise1
执行异步操作2
promise2
执行异步操作3
promise3

上面我们讲解了使用 then 方法可以执行 resolve 回调,那么 reject 回调如何执行呢?reject 的作用是把 Promise 的状态设置为 rejected,我们同样可以在 then 方法中执行。

例如,修改上面定义的 runAsync 函数,其中定义一个 flag 变量,默认为 false,判断 flagtrue 时,使用 resolve 回调传递值,否则,使用 reject 回调传递值。代码如下:

function runAsync(){
     flag=false
     var p = new Promise(function(resolve, reject){
          setTimeout(function(){
               if(flag){
                     console.log('执行异步操作')
                     resolve('promise')
               }
               else
                     reject('执行异步操作失败')
          }, 1000)
     })
     return p
}

然后在 Promise 对象的 then 方法中分别执行 resolve 回调和 reject 回调,代码如下:

runAsync()
     .then(function(data){
               console.log(data);
           },
          function(err){
               console.log(err);
          })

运行上面修改后的代码,由于 flag 变量为 false,所以输出结果为:

执行异步操作失败

除了 then 方法,Promise 对象还提供了一个 catch 方法,也可以执行 reject 回调,其使用方法与 then 类似。例如,上面代码可以修改如下:

runAsync()
     .then(function(data){
          console.log(data);
     })
     .catch(function(err){
          console.log(err);
     });

为什么使用async/await

ES2015 中引入 Promise 主要是为了解决异步回调的问题,但是由于它自身语法的复杂性,在 ES2017 标准中引入了 async/awaitasync/await 减少了 Promise 的样板,并且减少了 Promise 链式调用的 “不破坏链条” 的限制,它使得代码看起来像是同步的,但它是异步的并且在后台无阻塞。因此,通过使用 async/await 实现异步编程是一种更好的方式。

async/await的使用

通过前面的讲解,我们知道 ES2015 标准下的异步函数会返回 Promise,例如下面的代码:

const AsyncOper = () => {
     return new Promise(resolve => {
          setTimeout(() => resolve('执行操作'), 1000)
     })
}

在使用 async/await 对上面代码进行异步回调时,只需要在声明的函数前面加上 async 关键字,并在要调用的函数名前面加上 await 即可。这里需要注意的是,客户端函数必须被定义为 async。例如,下面代码中,要异步调用上面定义的 AsyncOper 函数,首先需要使用 async 关键字定义一个匿名的函数,然后在要调用的 AsyncOper 函数前面加上 await 关键字,代码如下:

const useAsync = async () => {
     console.log(await AsyncOper())
}

Node.js 中,在任何函数之前加上 async 关键字,就意味着该函数会返回 Promise,即使代码中没有显式返回 Promise,例如,下面两段代码是等效的:

//第1个函数
const Func1 = async () => {
     return '测试'
}
Func1().then(alert)                  //使用alert弹出信息测试函数
//第2个函数
const Func2 = () => {
     return Promise.resolve('测试')
}
Func2().then(alert)                  //使用alert弹出信息测试函数

【例9.3】使用 async/await 执行异步回调。(实例位置:资源包\源码\09\03)

WebStorm 中创建一个 .js 文件,其中主要演示使用 async/await 执行异步回调操作,代码如下:

var fs = require("fs") //引入fs模块
//定义异步函数,用来判断是否为文件夹
async function isDir(path) {
    return new Promise((resolve, reject)=> {
        fs.stat(path, (err,stats)=> {
            if (err) { //如果发生错误,则返回
                return
            }
            if (stats.isDirectory()) { //如果是文件夹返回true
                resolve(true);
            }else { //否则返回false
                resolve(false);
            }
        })
    })
}

var path = "D:\\测试文件夹" //指定当前路径
var dirArr = [] //用来记录遍历得到的所有文件夹名称
fs.readdir(path, async (err, data) => {
    if (err) {
        return
    }
    //遍历指定目录
    for (var i = 0; i < data.length; i++) {
        //异步调用isDir函数,判断是否为文件夹
        if (await isDir(path + '/' + data[i])) {
            dirArr.push(data[i]) //将文件夹的名称添加到数组中
        }
    }
    console.log(dirArr) //输出所有文件夹名称
})

代码中所指定的 D 盘测试文件夹中的原始内容如图9.8所示,运行程序,效果如图9.9所示,从结果可以看出,本程序只输出了指定路径下的所有文件夹名称。

image 2024 04 13 21 13 43 528
Figure 1. 图9.8 D盘测试文件夹中的原始内容
image 2024 04 13 21 14 02 490
Figure 2. 图9.9 使用async/await执行异步回调得到的文件夹名称

使用async/await异步编程的优点

本节讲解了两种异步编程的方式,分别是 Promiseasync/awaitasync/awaitPromise 相比,有很多优点,主要如下:

  • Promise 的出现解决了传统回调函数导致的 “地狱回调” 问题,但它的语法导致其发展成一个回调链,遇到复杂的业务场景时,这样的语法是不美观的;async/await 代码看起来更加简洁,使得异步代码看起来像同步代码,而 await 的本质其实就是可以提供等同于同步效果的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句。

  • async 修改的函数会默认返回一个 Promise 对象的 resolve 值,因此对 async 函数可以直接使用 then 方法,返回值就是 then 方法传入的函数。

  • async/await 是基于 Promise 实现的,可以说是改良版的 Promise,它不能用于普通的回调函数。

  • async/awaitPromise 一样,是非阻塞的。