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不能直接处理,但是可以将多个返回值包装为一个数组)。