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

promiseで外部API問い合わせを繰り返し行う

More than 1 year has passed since last update.

概要

Promiseを順次実行(直列実行)して外部APIに問い合わせ、各結果の値を格納して出力する方法です。
外部APIに多数の問い合わせをする際に、負荷軽減のため一気にリクエストを出さず1件ずつ応答を待って問い合わせるような場合を想定しています。

Promise基本

Promise-thenのメソッドを使うことで非同期処理をキレイに書くことができます。
単純な関数の例を示します。

sample.js
const task = (value) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => { // setTimeoutを使って非同期状態を作り外部API問い合わせを想定
      resolve(value * 2);
    }, 200);
  });
};

task(10)
.then(resp => {
  console.log(resp);
  return task(20)
})
.then(resp => {
  console.log(resp);
})

/*
実行結果
> 20
> 40
*/

reduceを組み合わせる

前述の「Promise基本」での場合はtaskに渡す値が固定しているため簡単ですが、問い合わせるデータが配列に入っているなどで個数も確定できない場合があります。
その場合はreduce関数を使います。
reduceは配列上の値を順番に取り出し、何らかの処理をして戻すことができるのですが、その各ステップにて「前回の処理結果」を参照することが出来ます。
この仕組みを利用して、順次処理を行います。

sample2.js
// 前回と同じ関数
const task = (value) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(value * 2);
    }, 200);
  });
};

const data = [10,20,30];
data.reduce((prev, current) => {
  return prev.then(() => {
    return task(current)
  });
}, Promise.resolve()); // 初期値

変数prevは前回の値、currentは今取り出した値が入ります。この場合dataの配列に入れた値をreduceしているので、currentには10,20,30の値が順次入ります。
なおprevは前回の値ですが、1回目は前回の値が存在しません。そのため最終行の"Promise.resolve()"を指定することで、1回目のprevにはこれが実行された結果がそのまま入ります。
これがreduceによってループ的に順次実行されるので、結果的にPromise.resolve()に対して.then(() => { return task() })が数珠つなぎに構成されることになり、順次実行の形が出来上がります。

イメージはこんな感じです。

example.js
Promise.resolve()
.then(() => { return task(10) })
.then(() => { return task(20) })
.then(() => { return task(30) })

戻り値を拾う

前述の例では実行そのものはうまく出来ますが、taskの戻り値を一切拾っていないので結果として何も起きません。笑
(例えばtask関数のなかでconsole.log(value)とか入れてみると、動いていることは分かります)
ので、この結果を拾って格納できるよう修正します。

・各taskの結果を格納
  ↓
・全部完了したら…
  ↓
・すべての結果をthenで戻す

という形で実装してみます。

sample3.js
// 前回と同じ関数
const task = (value) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => { // setTimeoutを使って非同期状態を作る
      resolve(value * 2);
    }, 200);
  });
};

// 値の格納を処理 + taskの実行を遅延する関数
var preTask = (value,result) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      task(value).then(resp => {
        resolve([...result,...resp]) // 格納用の配列に追加
      })
    },500)
  });
} 

const data = [10,20,30];
data.reduce((prev, current) => {
  return prev.then(resp => {
    return preTask(current,resp)
  });
}, Promise.resolve([])) // 初期で格納用の空の配列を渡す
.then(resp => {
  console.log(resp); // 結果の出力
});

preTaskという関数を追加して、taskから返ってきた結果の値を格納する処理をしています。
具体的には、preTaskがtaskを実行して返ってきた結果を、前回までの結果を格納している変数resultに追加しています。
そしてその合わせた結果をresolveで戻すことで、prev.then(resp => のところでrespに戻り、それをpreTaskの第2引数に渡すことで次のループに結果を引き継いでいます。
また最初のrespは空の配列が渡るようにPromise.resolve([])のところで空配列を渡しています。
そして最終的にdata.reduceに対して.thenでメソッドチェーンすることで、すべての値を格納した変数を受け取り、結果表示しています。

なおpreTaskでtaskの実行を遅延するためにsetTimeoutを入れていますが、単純な順次実行でしたらこちらは不要です。
(この部分をそのまま外しても動作するはず)
この処理は、例えばtaskで外部APIなどに問い合わせる際に、連続して問い合わせることによる短時間集中のリクエストエラーを回避することを想定しています。

まとめ

以上Promiseとreduceでの外部APIの問い合わせ方法でした。単純にPromiseとreduceでの順次実行方法は検索すればたくさん載っていますが、順次実行の各値を格納していくいい方法が見つからなかったので書いてみました。
誰かの手助けになりますように。

tomipetit
LINKA ASSOCIATES INC. Web Developper,Engineer. Toyohashi Aichi.
https://tomipetit.me
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