Node.js+Webpack开发实战
上QQ阅读APP看书,第一时间看更新

3.5 Node.js的异步编程风格

在本书的第1章介绍过Node.js是一个异步运行环境,异步意味着调用函数后结果不是立即返回,而是在未来的时刻再通知给调用者。Node.js使用最广泛的异步编程风格是基于回调函数来实现的。

截止本书出版时,Node.js可以使用以下几种异步编程风格:

· 回调函数

· Promise

· async/await

回调函数是最早出现的,也是最烦琐的,实际应用中不推荐以回调函数的形式进行异步调用。原因是多个异步操作有顺序依赖时,会产生如下代码:

3.5.1 回调函数

Node.js异步编程是通过回调函数来实现的,但不能说使用回调函数就异步了。

如下代码是基于回调函数的实现,但不是异步的:

function test(callback) {
  callback(1);
}

test(function(data) {
  console.log(data);
});

回调函数在完成任务后就会被调用,Node.js使用了大量回调函数,几乎所有的API都支持回调函数。

由于调用接口存在成功或失败的情况,而基于回调函数的编程无法使用标准JS中的抛出错误和捕获错误的方法。因此只能将错误对象作为回调参数来调用回调函数。

Node.js中回调函数的风格是统一的,这样给我们编程带来了很大的方便。异步函数的签名如下:

func(param..., callback(Error, data))

· param调用API的参数。如读取文件时传递的文件路径,支持多个参数。

· callback(Error, data...)回调函数。Node.js中回调函数的第一个参数永远是Error对象,之后才是调用成功的结果,如果没有出错,第一个参数为null。

例如,我们读取位于桌面的data.txt文件:

3.5.2 Promise

1.基本知识

Promise对象用于表示一个异步操作的最终完成(或失败)及其结果值。

一个Promise有以下几种状态:

· pending:初始状态

· fulfilled:操作成功

· rejected:操作失败

Promise只会从pending转换为fulfilled或者rejected,整个转换只发生一次。

Promise构造函数接收一个执行函数,该函数接收resolve和reject两个回调函数,当执行函数运行成功时需调用resolve,执行错误时需调用reject。

Promise构造函数的签名如下:

function Promise(function(resolve, reject): Promise {
   // 原来的异步逻辑
});

一旦Promise发生状态变化,就会触发then方法,then方法签名如下:

Promise.prototype.then = function(onFulfilled[, onRejected]): Promise

· onFulfilled Promise:执行成功时回调。

· onRejected Promise:执行出错时回调,该参数是可选的。

· then方法:返回一个新的Promise对象,因此Promise支持链式调用。

由于then的第二个参数onRejected参数是可选的,因此Promise的原型上提供了catch方法来捕获异步错误,catch方法签名如下:

Promise.prototype.catch = function(onRejected): Promise

· onRejected Promise:执行出错时回调。

· catch方法:返回一个新的Promise对象。

2.基本使用

Promise是为了解决异步编程问题而出现的,因此可以基于Promise来优化上文中读取文件的例子:

3.链式调用

Promise的then或catch回调函数的返回值会作为下一个then/catch的输入参数,因此可以通过链式Promise来扁平化嵌套的回调函数。

如果需要依次读取两个文件,那么就需要嵌套一层回调函数。如果依赖的异步操作越多,响应的嵌套层级也会越大,给代码的可读性和可维护性带来困难。

多个Promise链式调用时一旦有一个出错,整个调用链就会终止,然后回调catch函数。

通过链式调用,困扰多年的Node.js回调嵌套问题终于得到了第一次解决。

4.其他操作
Promise.resolve(value)

返回一个状态由value决定的Promise对象。value有以下几种取值:

· Promise。value本身是Promise的情况下,返回的Promise值由value这个Promise决定。

· 基本类型/空/或不带then方法的对象。返回的Promise值为value,状态为fulfilled。

Promise.reject(reason)

返回一个状态为失败的Promise,通常情况下会传递Error对象作为reason。

Promise.all(promises)

接收一个Promise数组,返回一个新的Promise对象。

· Promise数组中所有Promise都成功执行的情况下,返回的Promise最终会触发成功。

· Promise数组中只要有一个Promise执行失败,返回的Promise最终会执行失败。

Promise.race(promises)

接收一个Promise数组,返回一个新的Promise对象。

当Promise数组中任意一个Promise执行成功或失败,返回的Promise则立即成功或失败。

3.5.3 async/await

async和await关键字是ES2017中新添加的关键字,本质上是Promise的语法糖,使得能够像同步代码一样编写异步代码。

async/await本质是语法糖,因此尽管编程风格与同步类似,但是不会阻塞JS线程。

下面使用Promise小节中依次读取两个文件的需求为例:

可以看到readFiles函数中的代码跟同步编程风格一致(忽略async/await关键字)。

针对Promise调用链太长的问题,async/await提供了一个优美的解决方案,确实让异步编程变得简单了。

基本语法
(1)async

async只能放在函数声明之前,支持普通函数、箭头函数和类函数。被修饰的函数不管返回什么值,最终都会返回Promise。

· 函数返回基本值/空/或不带then方法的对象时,Promise的结果为该值,状态fulfilled。

· 函数抛出错误时,Promise状态为rejected,reason为抛出的错误对象。

· 函数本身返回一个Promise时,最终的Promise结果为该Promise的结果。

由于被async修饰的函数最终都会返回Promise,因此需要使用then才可以获得Promise的执行结果,直接调用函数只能得到一个Promise。

(2)await

await只能在被async修饰的函数内部调用,await可以放在任何返回Promise的函数前,Promise执行成功的情况下,await语句将返回Promise的成功值,Promise执行错误的情况下,await语句将抛出错误,通过try/catch捕获即可。

示例:包装XMLHttpRequest

几乎所有callback类型的异步函数都可以包装为Promise(特殊情况就是callback有多个返回值的情况,Promise不能直接处理,但是可以将多个返回值包装为一个数组)。