Edited at

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

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