LoginSignup
2
3

More than 5 years have passed since last update.

Node.jsの非同期処理を複数回繰り返す

Last updated at Posted at 2018-10-14

Node.js v10.11.0 を使用しています。

同期処理のループ

まずはただのループの例。1から5までの数字を足して出力します。
for文は嫌いですが、数字のシークエンスを用意してくれるAPIがネイティブにないのだから仕方ない。

sync-loop.js
// 同期ループ
let sum = 0
for (let i = 1; i <= 5; i++) {
  sum += i
  console.log(`Add ${i}: ${sum}`)
}

// 同期ループ後の処理
console.log(`Result: ${sum}`)
output.txt
Add 1: 1
Add 2: 3
Add 3: 6
Add 4: 10
Add 5: 15
Result: 15

非同期処理(setTimeout)のループ

setTimeoutで足し算を非同期に行います。
ループのイメージを掴む為、まずはベタ書きしてみます。

下書き

settimeout-loop-draft.js
// 経過時間計測用
const beginDate = Date.now()
const showElasped = () => {
  console.log(`Elasped: ${Date.now() - beginDate}ms`)
}

// 非同期ループ
let sum = 0
let i = 1
setTimeout(() => {
  sum += i
  console.log(`Add ${i}: ${sum}`)
  showElasped()
  i++
  setTimeout(() => {
    sum += i
    console.log(`Add ${i}: ${sum}`)
    showElasped()
    i++
    setTimeout(() => {
      sum += i
      console.log(`Add ${i}: ${sum}`)
      showElasped()
      i++
      setTimeout(() => {
        sum += i
        console.log(`Add ${i}: ${sum}`)
        showElasped()
        endCompute()
      }, i * 500)
    }, i * 500)
  }, i * 500)
}, i * 500)

// 非同期ループ終了時の着地先
const endCompute = () => {
  console.log(`Result: ${sum}`)
  showElasped()
}
output.txt
Add 1: 1
Elasped: 513ms
Add 2: 3
Elasped: 1517ms
Add 3: 6
Elasped: 3024ms
Add 4: 10
Elasped: 5024ms
Result: 10
Elasped: 5025ms

お手本のようなコールバック地獄ですね。
4まで足したところで満足しました。

さて、この繰り返しをfor文で再現するのは難しいのではないでしょうか。少なくとも自分は思いつきません。
というかどうみても再帰の形をしているように思います。
ということで再帰で実装してみましょう。

清書

settimeout-loop.js
// 経過時間計測用
const beginDate = Date.now()
const showElasped = () => {
  console.log(`Elasped: ${Date.now() - beginDate}ms`)
}

// 非同期ループ
let sum = 0
const setAddProcess = i => {
  if (i > 5) {
    endCompute()
    return
  }
  setTimeout(() => {
    sum += i
    console.log(`Add ${i}: ${sum}`)
    showElasped()
    setAddProcess(i + 1)
  }, i * 500)
}
setAddProcess(1)

// 非同期ループ終了時の着地先
const endCompute = () => {
  console.log(`Result: ${sum}`)
  showElasped()
}
output.txt
Add 1: 1
Elasped: 511ms
Add 2: 3
Elasped: 1516ms
Add 3: 6
Elasped: 3017ms
Add 4: 10
Elasped: 5022ms
Add 5: 15
Elasped: 7524ms
Result: 15
Elasped: 7525ms

iは処理から処理へ受け渡していますが、sumはトップレベルに置きっ放しにしてみました。
どちらの変数もどちらの形でも扱えると思います。

非同期処理(Promise)のループ

ここでせっかくなのでPromiseを使ってみます。

下書き

promise-loop-draft.js
// 経過時間計測用
const beginDate = Date.now()
const showElasped = () => {
  console.log(`Elasped: ${Date.now() - beginDate}ms`)
}

// 非同期ループ
let sum = 0
Promise.resolve(1)
  .then(i => {
    return new Promise(resolve => {
      setTimeout(() => {
        sum += i
        console.log(`Add ${i}: ${sum}`)
        showElasped()
        resolve(i + 1)
      }, i * 500)
    })
  })
  .then(i => {
    return new Promise(resolve => {
      setTimeout(() => {
        sum += i
        console.log(`Add ${i}: ${sum}`)
        showElasped()
        resolve(i + 1)
      }, i * 500)
    })
  })
  .then(i => {
    endCompute()
  })

// 非同期ループ終了時の着地先
const endCompute = () => {
  console.log(`Result: ${sum}`)
  showElasped()
}
output.txt
Add 1: 1
Elasped: 514ms
Add 2: 3
Elasped: 1520ms
Result: 3
Elasped: 1521ms

2まで足したところで満足しました。
怠惰はプログラマーの美徳です。

コールバック地獄がメソッドチェーン地獄になり下がりました。
この繰り返しは簡単ですね。

Promise.resolve()
  .then(() => console.log('foo'))
  .then(() => console.log('bar'))

はこのように書き換えられます。

let promise = Promise.resolve()
promise = promise.then(() => console.log('foo'))
promise = promise.then(() => console.log('bar'))

清書

promise-loop.js
// 経過時間計測用
const beginDate = Date.now()
const showElasped = () => {
  console.log(`Elasped: ${Date.now() - beginDate}ms`)
}

// 非同期ループ
let sum = 0
let addPromise = Promise.resolve(1)
for (let times = 1; times <= 5; times++) {
  addPromise = addPromise.then(i => {
    return new Promise(resolve => {
      setTimeout(() => {
        sum += i
        console.log(`Add ${i}: ${sum}`)
        showElasped()
        resolve(i + 1)
      }, i * 500)
    })
  })
}
addPromise = addPromise.then(i => {
  endCompute()
})

// 非同期ループ終了時の着地先
const endCompute = () => {
  console.log(`Result: ${sum}`)
  showElasped()
}
output.txt
Add 1: 1
Elasped: 510ms
Add 2: 3
Elasped: 1517ms
Add 3: 6
Elasped: 3023ms
Add 4: 10
Elasped: 5024ms
Add 5: 15
Elasped: 7526ms
Result: 15
Elasped: 7526ms

非同期処理(Promise)の動的ループ

さて、しれっと誤魔化していましたが、実は上記のPromiseを使う前と後の例では、やってることが微妙に違います。

Promiseを使う前は、2を足す処理が終わる頃に3を足す処理を、3を足す処理が終わる頃に4を足す処理を追加していました。
Promiseを使う例では、はじめに1〜5を足す処理を全て繋げた形で一気に追加しています。

このやり方は、今回のようにループする回数が固定な場合は使えますが、非同期処理を実行してみないとループ回数が分からないケースでは使えません(例えば、10を超えたら足すのをやめる、等)。

Promiseを使ったまま以前の柔軟性を維持するには、やはり再帰しかないんじゃないでしょうか。
具体的には、非同期処理が終わる頃に自身が属すPromiseチェーンに次のthenを足していくやり方です。

というわけで、「10を超えたら足すのをやめる」ループをPromiseと再帰処理でやってみます。

promise-loop-dynamic.js
// 経過時間計測用
const beginDate = Date.now()
const showElasped = () => {
  console.log(`Elasped: ${Date.now() - beginDate}ms`)
}

// 非同期ループ
let sum = 0
let promise = Promise.resolve(1)
const addAddPromise = () => {
  promise = promise.then(i => {
    return new Promise(resolve => {
      setTimeout(() => {
        sum += i
        console.log(`Add ${i}: ${sum}`)
        showElasped()
        if (sum <= 10) {
          addAddPromise()
        } else {
          addEndPromise()
        }
        resolve(i + 1)
      }, i * 500)
    })
  })
}
const addEndPromise = () => {
  promise = promise.then(i => {
    endCompute()
  })
}
addAddPromise()

// 非同期ループ終了時の着地先
const endCompute = () => {
  console.log(`Result: ${sum}`)
  showElasped()
}
output.txt
Add 1: 1
Elasped: 510ms
Add 2: 3
Elasped: 1518ms
Add 3: 6
Elasped: 3020ms
Add 4: 10
Elasped: 5026ms
Add 5: 15
Elasped: 7526ms
Result: 15
Elasped: 7527ms

こうなるとPromiseのメリットは特に無い気がします。
素直に上記のPromiseを使わない書き方をした方が良いんじゃないでしょうか。

結論

なんかライブラリとか使った方が良さそうですね。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3