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に出しちゃえということで公開しました。