Help us understand the problem. What is going on with this article?

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の紹介でした。

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした