Node.js async functions 最佳实践

从 Node.js 7.6 开始, Node.js 采用了支持 async functions 的新版 V8 引擎。2017年10月31日, Node.js 8 正式成为长期支持的版本,我们没有理由不在代码中使用 async functions 了。在这篇文章中,我会简单地向你说明什么是 async functions 以及它们是如何改变我们编写 Node.js 应用的方式的。

什么是 async functions ?

async functions 能让你像写同步代码一样编写基于 PromisePromise-based ) 的代码。当你用 async 关键字定义了一个函数,你就可以在该函数内部使用 await 关键字。当这个 async function 被调用时,它会返回一个 Promise。当这个 async function 返回一个值,被返回的 Promise 的状态会变为 fufilled,若 async function 抛出一个错误, Promise 的状态变为 rejected

await 关键字用来等待一个 Promise 被解决( resolved )并返回结果( the fulfilled value )。如果传递给 await 关键字的值不是一个 Promise,那么它会被转换成一个 resolved Promise

const rp = require( 'request-promise' );

async function main() {
  const result = await rp( 'https://google.com' );
  const twenty = await 20;

  // sleeeeeeeeping for a second 💤

  await new Promise( resolve => {
    setTimeout( resolve, 1000 );
  } );

  return result;
}

main.then( console.log ).catch( console.error );

迁移到 async functions

如果你的 Node 应用已经在使用 Promise,那就只需要开始 await 你的 Promise,而不是像以往一样把它们链接起来。

如果你的应用是使用 callback 构建的,那向 async functions 的迁移就应该逐步进行。你可以在添加新功能的时候使用这个新技术。如果你不得不使用之前的代码,那就简单地用 Promise 把它们包装起来。

可以使用内建的 util.promisify 方法来完成这件事情:

const util         = require( 'util' );
const { readFile } = require( 'fs'   );

const readFileAsync = util.promisify( readFile );

async function main() {
  const result = await readFileAsync( '.gitignore' );

  return result;
}

main.then( console.log ).catch( console.error );

async functions 的最佳实践

express 中使用 async functions

由于 expressPromise 的支持非常好,在 express 中使用 asyncs functions 非常简单:

const express = require( 'express' );

const app = express();

app.get( '/', async ( request, response ) => {
  // awaiting Promise here
  // if you just wait a single promise, you could simply return with it,
  // no need to await for it

  const result = await getContent();

  response.send( result );
} );

app.listen( process.env.PORT );

上面的例子有一个严重的问题,如果 Promiserejectexpress 的路由处理函数会被挂起,因为没有任何处理错误的行为。

为了解决这个问题,你需要把你的异步处理函数放在一个能处理异常的函数中:

const awaitHandlerFactory = ( middleware ) => {
  return async ( req, res, next ) => {
    try {
      await middleware( req, res, next );
    } catch ( err ) {
      next( err );
    }
  };
}

// and use it this way:

app.get( '/', awaitHandlerFactory( async ( request, response ) => {
  const result = await getContent();

  response.send( result );
} ) );

并行执行

想象你在做类似的事情,一个操作需要两个输入,一个来自数据库,另一个来自外部服务:

async function main() {
  const user    = await Users.fetch( userId );
  const product = await Products.fetch( productId );

  await makePurchase( user, product );
}

在这个例子中,会发生如下事件:

  • 你的代码会先获取 user 信息,
  • 然后获取 product 信息,
  • 最后完成购买

如你所见,你可以同时做前两件事情,因为它们之间并没有依赖关系。你可以通过 Promise.all 方法来做这件事:

  async function main() {
    const [ user, product ] = await Promise.all( [
      Users.fetch( userId );
      Products.fetch( productId );
    ] );

    await makePurchase( user, product );
  }

在另外一些情况下,你可能只需要最先被 resolvePromise 的结果,在上面的例子中,你可以使用 Promise.race 方法。

错误处理

考虑下面的代码示例:

async function main() {
  await new Promise( ( resolve, reject ) => {
    reject( new Error( '💥' ) );
  } );
}

main.then( console.log );

在运行这个代码段时,你会在你的终端中看到类似这样的信息:

(node:69738) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error : 💥
(node:69738) [DEP0018] DeprecationWarning: Unhandled promise rejections deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

在比较新的版本的 Node.js 中,如果 Promise rejections 没有被处理,它会让整个 Node 进程挂掉。正因为如此,在必要的时候,你应该使用 try-catch 块:

const util = require( 'util' );

async function main() {
  try {
    await new Promise( ( resolve, reject ) => {
      reject new Error( '💥' );
    } );
  } catch ( err ) {
    // handle error case
    // maybe throwing is okay depending on your use-case
  }
};

main.then( console.log ).catch( console.error );

但是,使用 try-catch 块可能会隐藏一些重要的异常,比如你希望被抛出的系统错误。

更复杂的控制流

async 是最早的关于 Node.js 异步控制流的库之一,它是由 Caolan McMahon 创建的。它提供一些 asynchronous helpers,比如:

  • mapLimit,
  • filterLimit,
  • concatLimit,
  • priorityQueue.

如果你不想为了实现同样的逻辑而重复造轮子,而且你也希望使用一个月下载量达到5千万、久经实践验证的库,你可以在使用 async functions 的时候结合 util.promisify 利用上述的方法:

const util  = require( 'util'  );
const async = require( 'async' );

const numbers = [ 1, 2, 3, 4, 5 ];

mapLimitAsync = util.promisify( async.mapLimit );

async function main() {
  return await mapLimitAsync( numbers, 2, ( number, done ) => {
    setTimeout( function () {
      done( null, number * 2 )
    }, 100 );
  } );
}

main.then( console.log ).catch( console.error );