Node.js v10.11.0 を使用しています。
同期処理のループ
まずはただのループの例。1から5までの数字を足して出力します。
for文は嫌いですが、数字のシークエンスを用意してくれるAPIがネイティブにないのだから仕方ない。
// 同期ループ
let sum = 0
for (let i = 1; i <= 5; i++) {
sum += i
console.log(`Add ${i}: ${sum}`)
}
// 同期ループ後の処理
console.log(`Result: ${sum}`)
Add 1: 1
Add 2: 3
Add 3: 6
Add 4: 10
Add 5: 15
Result: 15
非同期処理(setTimeout)のループ
setTimeoutで足し算を非同期に行います。
ループのイメージを掴む為、まずはベタ書きしてみます。
下書き
// 経過時間計測用
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()
}
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文で再現するのは難しいのではないでしょうか。少なくとも自分は思いつきません。
というかどうみても再帰の形をしているように思います。
ということで再帰で実装してみましょう。
清書
// 経過時間計測用
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()
}
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を使ってみます。
下書き
// 経過時間計測用
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()
}
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'))
清書
// 経過時間計測用
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()
}
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と再帰処理でやってみます。
// 経過時間計測用
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()
}
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を使わない書き方をした方が良いんじゃないでしょうか。
結論
なんかライブラリとか使った方が良さそうですね。