Node.js中常见的异步/等待设计模式

Node.js中的异步/等待打开了一系列强大的设计模式。现在可以使用基本语句和循环来完成过去采用复杂库或复杂承诺链接的任务。我已经用co编写了这些设计模式,但异步/等待使得这些模式可以在vanilla Node.js中访问,不需要外部库。iffor

Node.js中常见的异步/等待设计模式

重试失败的请求

其强大之await处在于它可以让你使用同步语言结构编写异步代码。例如,下面介绍如何使用回调函数使用superagent HTTP库重试失败的HTTP请求。

const superagent = require('superagent');

const NUM_RETRIES = 3;

request('http://google.com/this-throws-an-error', function(error, res) {
  console.log(error.message); // "Not Found"
});

function request(url, callback) {
  _request(url, 0, callback);
}

function _request(url, retriedCount, callback) {
  superagent.get(url).end(function(error, res) {
    if (error) {
      if (retriedCount >= NUM_RETRIES) {
        return callback && callback(error);
      }
      return _request(url, retriedCount + 1, callback);
    }
    callback(res);
  });
}

不是太难,但涉及递归,对于初学者来说可能非常棘手。另外,还有一个更微妙的问题。如果superagent.get().end()抛出一个同步异常会发生什么?我们需要将这个_request()调用包装在try / catch中以处理所有异常。必须在任何地方这样做都很麻烦并且容易出错。随着异步/ AWAIT,你可以写只用同等功能fortry/catch

const superagent = require('superagent');

const NUM_RETRIES = 3;

test();

async function test() {
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    try {
      await superagent.get('http://google.com/this-throws-an-error');
      break;
    } catch(err) {}
  }
  console.log(i); // 3
}

相信我,这是有效的。我记得我第一次尝试这种模式与合作,我感到莫名其妙,它实际工作。但是,下面的就不能正常工作。请记住,await必须始终在async函数中,而传递给forEach()下面的闭包不是async

const superagent = require('superagent');

const NUM_RETRIES = 3;

test();

async function test() {
  let arr = new Array(NUM_RETRIES).map(() => null);
  arr.forEach(() => {
    try {
      // SyntaxError: Unexpected identifier. This `await` is not in an async function!
      await superagent.get('http://google.com/this-throws-an-error');
    } catch(err) {}
  });
}

处理MongoDB游标

MongoDB的find()函数返回一个游标。游标基本上是一个具有异步next()函数的对象,它可以获取查询结果中的下一个文档。如果没有更多结果,则next()解析为空。MongoDB游标有几个辅助函数,如each(),,map()toArray(),猫鼬ODM增加了一个额外的eachAsync()函数,但它们都只是语法上的糖next()

没有异步/等待,next()手动调用涉及与重试示例相同的递归类型。使用async / await,你会发现自己不再使用助手函数(除了可能toArray()),因为用循环遍历游标for要容易得多:

const mongodb = require('mongodb');

test();

async function test() {
  const db = await mongodb.MongoClient.connect('mongodb://localhost:27017/test');

  await db.collection('Movies').drop();
  await db.collection('Movies').insertMany([
    { name: 'Enter the Dragon' },
    { name: 'Ip Man' },
    { name: 'Kickboxer' }
  ]);

  // Don't `await`, instead get a cursor
  const cursor = db.collection('Movies').find();
  // Use `next()` and `await` to exhaust the cursor
  for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
    console.log(doc.name);
  }
}

如果这对你来说不够方便,有一个TC39的异步迭代器建议可以让你做这样的事情。请注意,下面的代码并没有在Node.js的任何目前发布的版本工作,这只是什么是可能在未来的一个例子。

const cursor = db.collection('Movies').find().map(value => ({
  value,
  done: !value
}));

for await (const doc of cursor) {
  console.log(doc.name);
}

并行多个请求

上述两种模式都按顺序执行请求,只有一个next()函数调用在任何给定的时间执行。怎么样并行多个异步任务?让我们假装你是一个恶意的黑客,并且想要与bcrypt并行地散列多个明文密码。

const bcrypt = require('bcrypt');

const NUM_SALT_ROUNDS = 8;

test();

async function test() {
  const pws = ['password', 'password1', 'passw0rd'];

  // `promises` is an array of promises, because `bcrypt.hash()` returns a
  // promise if no callback is supplied.
  const promises = pws.map(pw => bcrypt.hash(pw, NUM_SALT_ROUNDS));

  /**
   * Prints hashed passwords, for example:
   * [ '$2a$08$nUmCaLsQ9rUaGHIiQgFpAOkE2QPrn1Pyx02s4s8HC2zlh7E.o9wxC',
   *   '$2a$08$wdktZmCtsGrorU1mFWvJIOx3A0fbT7yJktRsRfNXa9HLGHOZ8GRjS',
   *   '$2a$08$VCdMy8NSwC8r9ip8eKI1QuBd9wSxPnZoZBw8b1QskK77tL2gxrUk.' ]
   */
  console.log(await Promise.all(promises));
}

Promise.all()函数接受一组承诺,并返回一个承诺,等待数组中的每个承诺解析,然后解析为一个数组,该数组包含解析的原始数组中每个承诺的值。每个bcrypt.hash()调用都会返回一个promise,所以promises在上面的数组中包含一组promise,并且value的值await Promise.all(promises)是每个bcrypt.hash()调用的结果。

Promise.all()并不是您可以并行处理多个异步函数的唯一方式,还有一个Promise.race()函数可以并行执行多个promise,等待第一个解决的承诺并返回承诺解决的值。以下是使用Promise.race()async / await 的示例:

/**
 * Prints below:
 * waited 250
 * resolved to 250
 * waited 500
 * waited 1000
 */
test();

async function test() {
  const promises = [250, 500, 1000].map(ms => wait(ms));
  console.log('resolved to', await Promise.race(promises));
}

async function wait(ms) {
  await new Promise(resolve => setTimeout(() => resolve(), ms));
  console.log('waited', ms);
  return ms;
}

请注意,尽管Promise.race()在第一个承诺解决后解决,但其余的async功能仍然继续执行。请记住,承诺不可取消。

继续

异步/等待是JavaScript的巨大胜利。使用这两个简单的关键字,您可以从代码库中删除大量外部依赖项和数百行代码。您可以添加强大的错误处理,重试和并行处理,只需一些简单的内置语言结构。

上一篇:【愚公系列】2021年12月 二十三种设计模式(十)-外观模式(Facade Pattern)


下一篇:MySQL的CrashSafe和Binlog的关系