天天看點

Node.js中常見的異步/等待設計模式

Node.js中的異步/等待打開了一系列強大的設計模式。現在可以使用基本語句和循環來完成過去采用複雜庫或複雜承諾連結的任務。我已經用co編寫了這些設計模式,但異步/等待使得這些模式可以在vanilla Node.js中通路,不需要外部庫。

if

for

重試失敗的請求

其強大之

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,你可以寫隻用同等功能

for

try/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的巨大勝利。使用這兩個簡單的關鍵字,您可以從代碼庫中删除大量外部依賴項和數百行代碼。您可以添加強大的錯誤處理,重試和并行處理,隻需一些簡單的内置語言結構。

繼續閱讀