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

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark