JavaScript
Node.js
promise
es2017
AsyncAwait

async/awaitやPromiseで簡単に配列のイテレーションできるようにする

More than 1 year has passed since last update.

作ったモジュールをこの記事で紹介しようと思います。
GitHub: p-iteration (Promise-Iteration)

問題

async/awaitは非常に便利で、コードは同期処理の書き方に近くなったが、ストレートに書けない場合が割と出てくる。配列のイテレーションはその一つの問題。

例えば、Array#map()の利用の際にcallbackの中にawaitを使いたい場合がある。

async function getUsers (userIds) {
  const users = userIds.map(async userId => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  });
  // ... do some stuff
  return users;
}

だが、上記のコードは思う通り動かない。なぜなら、map()は渡されたcallbackを同期に実行するが、上記のコードに渡しているcallbackの中で非同期処理が行われているからだ。

awaitするにはasync functionで囲う必要があるので、async functionをcallbackとして渡した。async functionはいつもPromiseオブジェクトを返す。上記の場合だと、return response.json();は実行し終わった時点で返ってきたPromiseはresolveされるが、map()はPromiseがresolveされるのを待たないので、各イテレーションのPromiseはresolveされた時点でmap()の実行はもう終わっている。結果的にusersにPromiseの配列になっています。

下記のコードを実行して頂けると行動はわかりやすいと思う。

const users = [1, 2, 3].map(async (n) =>  {
  await "dummy";
  console.log(n);
  return n * 2;
});
console.log(users);
console.log("foo");

順番に [Promise, Promise, Promise] foo 1 2 3 が表示される。

map()を例として書いたが、この行動はmap()だけじゃなく、他のbuilt-in Iteration MethodsforEach()find()every()reduce()など)も同じ。工夫せずにcallbackの中に非同期処理を行うことはできない。こういった問題はStack Overflowによく出てきている

p-iterationで楽に解決

p-iterationを使うと、スムーズにcallbackに非同期処理をおこなえる。

map()の例

上記に書いた例をp-iterationで書いてみよう。

const { map } = require('p-iteration');

async function getUsers (userIds) {
  const users = await map(userIds, async userId => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  });
  // ... do some stuff
  return users;
}

forEach()

よく使われるArray#forEach()の例も書いてみよう。

const { forEach } = require('p-iteration');

function logUsers (userIds) {
  return forEach(userIds, async userId => {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    console.log(user);
  });
}

配列にPromiseが入っている場合

配列の中にもPromiseを入れることは可能。callbackに渡される前にPromiseはunwrapされる。

const { forEach } = require('p-iteration');
const fetchJSON = require('nonexistent-module');

async function logUsers () {
  const users = [
    fetchJSON('/api/users/125'), // returns a Promise
    { userId: 123, name: 'Jolyne', age: 19 },
    { userId: 156, name: 'Caesar', age: 20 }
  ];
  return forEach(users, (user) => {
    console.log(user);
  });
}

プレーンのPromiseでの利用の場合

async/awaitを利用しなくても、プレーンのPromiseの書き方は可能。

const { map } = require('p-iteration');

map([123, 125, 156], (userId) => fetch(`/api/users/${userId}`))
  .then((result) => {
    // ...
  })
  .catch((error) => {
    // ...
  });

他のメソッド

map()forEach()以外、ES5のbuilt-in Iteration Methodsの真似をして、find()findIndex()some()every()filter()reduce()も実装した。

詳細はドキュメンテーションに参考

おわりに

p-iterationの紹介でした。

このモジュールが役に立ったら、スターを頂けると嬉しいです。イシューもしくはプルリクエストも待っています!