以前も同じような記事を書いていたんですけど、最近TypeScript覚えたし、もう少しいい感じに書けないかな?と思ったので新しい記事にしてみました。
for ループ
まずは普通にループを書く。こんな感じの処理をしたい。
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log('END!!!');
まず、ループの各部分を別々の関数として切り出すことを考えます。
for (init(); condition(); increment()) {
loopBody();
}
次に、これらの関数がPromise
を返すとしたらどのようになるかを考えます。
…考えました。考えた結果がこちらです。
export interface LoopCallback {
init(): Promise<any>
condition(): Promise<boolean>
loopBody(): Promise<any>
increment(): Promise<any>
}
export function loop(callback: LoopCallback) {
return callback.init()
.then(function _loop() {
return callback.condition()
.then(result => {
if (result) {
return callback.loopBody()
.then(_ => callback.increment())
.then(_loop);
}
});
});
}
import { loop, LoopCallback } from './loop';
let i;
loop({
init: () => {
i = 0;
return Promise.resolve();
},
condition: () => {
return Promise.resolve(i < 10);
},
increment: () => {
i++;
return Promise.resolve();
},
loopBody: () => {
console.log(i);
return Promise.resolve();
}
}).then(() => {
console.log('END!!!');
})
前回の記事では複数の関数を渡していましたが、今回はinterface
を定義して一つのオブジェクトを渡すようにしてみました。
この方法のメリットは、ループの過程で変化していく状態などをクラスの中に隠蔽できることです。例えば以下のようにしてカウンター変数をクラスのプライベート変数に書くことができます。
class LoopImpl implements LoopCallback {
private counter: number;
init(): Promise<any> {
this.counter = 0;
return Promise.resolve();
}
condition(): Promise<boolean> {
return Promise.resolve(this.counter < 10);
}
loopBody(): Promise<any> {
console.log(this.counter);
return Promise.resolve();
}
increment(): Promise<any> {
this.counter++;
return Promise.resolve();
}
}
loop(new LoopImpl())
.then(_ => console.log('END!!!'));
配列の中身を処理する場合
普通に配列の中身を同期処理する場合はこのような感じで書けます。
let array: [] = getSomeArray();
array.forEach((data, index) => {
// 要素ごとの処理
});
しかし要素ごとの処理が非同期処理の場合は、このままでは処理が終わったことを検知できません。
let array: [] = getSomeArray();
array.forEach((data, index) => {
someMethod(data)
.then(_=> {
// ...
});
});
// arrayの要素全てを処理し終えた場合は??
そこでPromise.all()
を使います。Promise.all()
はPromiseの配列を受け取り、全てのPromiseがresolveされたときにresolveされるPromiseを返します。
let promises = [...];
Promise.all(promises)
.then(results => {
// promises全てが終わった時の処理
});
つまり、配列の要素一つ一つに関する処理をPromiseを返す関数で表現して、そのPromiseを全て配列に入れてPromise.all()に渡せば、配列全ての要素が終わった時にresolveされるPromiseができます。
let array: [] = getSomeArray();
let promises = [];
array.forEach((data, index) => {
promises.push(someMethod(data));
});
Promise.all(promises)
.then(_=>{
// 配列の要素を全て処理した後
});
配列の各要素を別の値に置き換えるArray.mapを使うともう少し簡単に書けます。
let array: [] = getSomeArray();
let promises = array.map((data, index) => {
return someMethod(data);
});
Promise.all(promises)
.then(_=>{
// 配列の要素を全て処理した後
});
ところで、この方法で配列の各要素を処理する場合、各要素の処理(上記の例ではsomeMethod(data)
)は全て同時に実行されます(厳密に言うとマルチスレッドではないので本当に同時ではないのですが、感覚としては同時です)。
通常はこの方法で問題ないのですが、時には各要素を順番に処理したいときがあります。こういう時は以下のようにPromiseをthen
でつなげる必要があります。
someMethod(item1)
.then(_=>someMethod(item2))
.then(_=>someMethod(item3))
...
Array.reduceを使うと、これがうまく表現できます。うまく表現した結果がこちらです。
function sequentialForEach<T>(array: T[], fn: (item: T, index: number, array: T[]) => Promise<any>): Promise<any> {
return array.reduce((promise, item, index, array) => {
return promise.then(_ => fn(item, index, array))
}, Promise.resolve());
}
上記sequentialForEach
関数には配列とコールバック関数を渡します。コールバック関数には配列の要素、インデックス、配列そのものが引数として渡されます。
// setTimeoutをPromiseでラップするだけの関数
function wait(time) {
return new Promise((resolve, reject) => {
setTimeout(resolve, time)
})
}
// 待機時間の配列
let nums = [
1000, 1500, 2000
]
sequentialForEach(nums, (num) => {
console.log('start waiting')
return wait(num)
.then(_ => console.log('waited ' + num + ' ms'))
})
// 全ての要素を処理し終えた時
.then(_ => console.log('END'))
ついでに、先程の同時に実行するパターンも関数化しておきましょうか。
function parallelForEach<T, R>(array: T[], fn: (item: T, index: number, array: T[]) => Promise<R>): Promise<R[]> {
let promises = array.map(fn);
return Promise.all(promises);
}
こちらのparallelForEach
関数も使い方はsequentialForEach
関数と同じです。
よくよく見ると戻り値の型が違ったりしますけど……sequentialForEach
で処理結果の配列を受け取るようにしようと思ったら結構コードが複雑になってしまったのでやめました。こうしたらできるよ!っていうのがありましたら教えてください。