JavaScriptNode.jsTypeScript

Node.jsで非同期処理を同期的に逐次実行する

APIのスロットリング対策など、逐次的に処理を走らせたい場合の覚書です。 コード 早速コードです。 result.push()しなくてもpにreturnすればよさそうですが、returnだと最後の値しか来なくなります。な […]

広告ここから
広告ここまで

APIのスロットリング対策など、逐次的に処理を走らせたい場合の覚書です。

コード

早速コードです。 result.push()しなくてもpにreturnすればよさそうですが、returnだと最後の値しか来なくなります。なのでややこしさを出さないためにもp: Promise<void>で何も返さないことを決めてしまった方が平和かなと思います。

// ダミー。1秒待ってresolveするだけの関数
const dummy = async () => {
  return new Promise(resolve => setTimeout(resolve, 1000))
}

// 本体
const sequentialPromise = async () => {
  // Promiseを準備する。ループ内で書き換えるのでletで初期化
  let p: Promise<void> = Promise.resolve()

  // ループさせる配列
  const arr = [1,2,3,4,5]

  // 処理結果を保存する配列
  const results: number[] = []
  
  // ループを開始
  arr.forEach(i => {
    // 1要素ずつPromise.resolveしていく
    p = p.then(async () => {

      // これはちゃんとwaitできているか確認する用
      const start = moment()
      console.log(`Start: ${start.toISOString()}`)
      console.log(`Number: ${i}`)

      // 非同期処理
      await dummy()

      // 結果をpushする
      results.push(i + 1)

      // これもwaitできているか確認する用
      console.log(`End ${moment().toISOString()}`)
      console.log(`${moment().diff(start, 'seconds')} sec`)
      console.log(' ')
    })
  })

  // ここでwaitしてやらないとpがすべてresolveされるまえに終わってしまう。
  await p

  // Promise.all([1,2,3].map(...))のように結果を取りたいならここでreturnしてやる
  return results
}

sequentialPromise().then(result => console.log(result)

これを実行すると、以下のような出力がでます。

Number: 1
Start: 2019-08-30T07:35:00.175Z
End 2019-08-30T07:35:01.182Z
1 sec
 
Number: 2
Start: 2019-08-30T07:35:01.184Z
End 2019-08-30T07:35:02.188Z
1 sec
 
Number: 3
Start: 2019-08-30T07:35:02.188Z
End 2019-08-30T07:35:03.194Z
1 sec
 
Number: 4
Start: 2019-08-30T07:35:03.194Z
End 2019-08-30T07:35:04.200Z
1 sec
 
Number: 5
Start: 2019-08-30T07:35:04.200Z
End 2019-08-30T07:35:05.206Z
1 sec
 
[ 2, 3, 4, 5, 6 ]

1秒waitしながら逐次に処理を実行し、すべての配列に+1した結果が帰ってきています。

非同期にやる場合はPromise.all

Promise.allの方がシンプルにかけます。が、こちらは非同期に処理が走る点に注意が必要です。

// ダミー。1秒待ってresolveするだけの関数
const dummy = async () => {
  return new Promise(resolve => setTimeout(resolve, 1000))
}

const asyncFunc = async () => {
  // ループさせる配列
  const arr = [1,2,3,4,5]

  // Promise.all版の処理
  const result = await Promise.all(arr.map(async i => {
    const start = moment()
    console.log(`Number: ${i}`)
    console.log(`Start: ${start.toISOString()}`)
    await dummy()
    console.log(`End ${moment().toISOString()}`)
    console.log(`${moment().diff(start, 'seconds')} sec`)
    console.log(' ')
    return i + 1
  }))
  return result
})

asyncFunc().then(result => console.log(result))

実行結果はこちら。

Number: 1
Start: 2019-08-30T07:52:43.862Z
Number: 2
Start: 2019-08-30T07:52:43.862Z
Number: 3
Start: 2019-08-30T07:52:43.863Z
Number: 4
Start: 2019-08-30T07:52:43.863Z
Number: 5
Start: 2019-08-30T07:52:43.863Z
End 2019-08-30T07:52:44.866Z
1 sec
 
End 2019-08-30T07:52:44.866Z
1 sec
 
End 2019-08-30T07:52:44.867Z
1 sec
 
End 2019-08-30T07:52:44.867Z
1 sec
 
End 2019-08-30T07:52:44.867Z
1 sec
 
[ 2, 3, 4, 5, 6 ]

結果は同じですが、並列実行されているためにログの順番がバラバラになっていることがわかります。

関数にしてみる

毎回実装を書くのは面倒なので、関数化しました。

const sequentialPromise = async <T = any, R = any>(targets: T[], callback: (prop: T) => Promise<R>): Promise<R[]> => {
  const results: R[] = []
  let p: Promise<void> = Promise.resolve()
  targets.forEach(target => {
    p = p.then(async () => {
      const result = await callback(target)
      results.push(result)
    })
  })
  await p
  return results
}

使う時はこんな感じです。

sequentialPromise<number, string>([1,2,3,4,5], async (i) => {
  const start = moment()
  console.log(`Number: ${i}`)
  console.log(`Start: ${start.toISOString()}`)
  await dummy()
  console.log(`End ${moment().toISOString()}`)
  console.log(`${moment().diff(start, 'seconds')} sec`)
  console.log(' ')
  return `${i} + 2 = ${i + 2}`
}).then(r => console.log(r))

そしてライブラリへ

ここまできたらnpmに出しちゃえということで公開しました。

@hideokamoto/sequential-promise

ブックマークや限定記事(予定)など

WP Kyotoサポーター募集中

WordPressやフロントエンドアプリのホスティング、Algolia・AWSなどのサービス利用料を支援する「WP Kyotoサポーター」を募集しています。
月額または年額の有料プランを契約すると、ブックマーク機能などのサポーター限定機能がご利用いただけます。

14日間のトライアルも用意しておりますので、「このサイトよく見るな」という方はぜひご検討ください。

広告ここから
広告ここまで

Related Category posts