TL;DR
- 配列に突っ込んでreduceすると簡単に直列処理が書けて、さらに処理開始をコントロールできてうれしい
- reduceした後に配列に処理が増えると、後から増えた分は実行されない
- 処理が順次増えるときは配列に入れるのではなくthenメソッドでさっさと繋ぐといい
- Async Functionsはよ
この記事は最低限のPromiseへの理解を要します。
Promiseを返す関数の直列実行には本来Promise.prototype.then
を使えばよい。
function f(wait) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(wait);
resolve(wait * 2);
}, wait);
});
};
Promise.resolve(100)
.then(f)
.then(f)
.then(f);
// 100 (100ms後)
// 200 (さらに200ms後)
// 400 (さらに400ms後)
Promiseを返す関数や、Promiseを返す関数に渡すパラメータを配列に持っておいて、Array.prototype.reduce
する方法もよく用いられる。
const arr = [
f, f, f,
(c) => Promise.all([f,f].map((x) => x(c)))
];
function log(x) {
console.log(x);
return x;
}
var promise = arr.reduce((m, p) => m.then(p), Promise.resolve(100))
// 100 (100ms後)
// 200 (さらに200ms後)
// 400 (さらに400ms後)
// 800 (さらに800ms後)
const summate = (x) => x.reduce((a,b) => a+b);
promise = promise.then(summate).then(log);
// 3200 (immediately)
配列に入ったPromiseを返す関数たちをthenメソッドで繋ぎ換えるので、最終結果はPromiseになる。つまり更にthenメソッドで処理を繋ぐことができる。
reduceメソッドを使ってうれしいこと
reduceメソッドを使う方法がthenメソッドによるチェインより便利なのは、処理の開始をコントロールできることにある。配列に入っている段階ではただの処理の列であり、reduceメソッドによってresolvedなPromiseに連ねられたところで処理を開始する。
reduceメソッドを使ってつらいこと
キューイングした配列からreduceメソッドで全体のPromiseを得た後に配列に追加された処理は実行されない。reduceメソッドは呼び出した時点の配列に対して適用されるからだ。
処理開始後にも順次処理を追加したい場合は、reduceメソッドで返したPromiseに対して、更にthenメソッドで連ねる必要がある。これでは処理開始の前後で処理を追加するAPIが変化することになる。処理フローを正確に理解していなければ思わぬバグにつながるだろう。
APIの統一
上述しているが、Promiseを返す関数の配列にreduceメソッドを使用しているのは、単にthenメソッドによる連なりとして繋ぎ直しているだけである。ならば、最初からそのように書けばいいのだ。
let t = Promise.resolve(100);
t = t.then(f);
t = t.then(f).then(f);
t = t.then(f).then(log);
もちろん、すぐに一連の処理の長さが確定する程度の小さい規模では配列に押し込めてreduceメソッドを使い、大域的にはthenメソッドで連ねるのもいい。大域的に配列を使ってキューイングすると実行開始前後でAPIが乱れることを指摘しているのであって、配列にしてreduceメソッドを使うこと自体は便利だ。
処理開始の操作/処理完了の通知
直列処理を原則としてthenメソッドで書くとして、reduceメソッドを使ったときの利点である実行開始タイミングのコントロールはどうしたらいいか。
これは、reduceメソッドの初期値に与えるPromiseを未解決のまま渡すことにし、要求に応じてresolveすることにしよう。
順次継ぎ足される一連の処理の終了を得るにはどうしたらいいか。そのままthenメソッドで処理を足しても、その時点までの完了を見ることができるのみで、全体としての終了を確認できない。
ならば、一連の処理全体の状態を示すPromiseを持っておき、終わったときにresolveすればよい。このPromiseオブジェクトを露出しておけば、終了時に処理が呼べる。
未解決のPromiseを得る
jQuery Deferredでは次のように未解決なPromiseを得ることができ、外部からresolve/rejectをコントロールすることができた。
const d = new $.Deferred;
const dp = d.promise();
// d.resolve();
// d.reject();
ES6 Promiseでも同様に外部から状態のコントロールをしたいときは、自分でコールバックをリークする必要がある。
let resolve, reject;
const pendingPromise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// resolve();
// reject();
const thunk = x => () => x;
Promise.resolve()
.then(thunk(100))
.then(log)
.then(thunk(pendingPromise))
.then(thunk(200), thunk(300))
.then(log);
// 100 (immediately)
// 200 (after resolve())
補足として、このように外部からそのPromiseのresolve/rejectを決定できるものは、Promise.prototype.defer
として仕様化が提案されていたが、この提案は却下された。
ライブラリとしてpromise-deferが存在する。
結果
次のように、順次非同期な処理が追加される場合をPromiseで記述できる。
let tasksResolve, tasksReject;
const tasksPromise = new Promise((res, rej)=>{
tasksResolve = res;
tasksReject = rej;
});
const terminate = () => tasksResolve();
tasksPromise.then(()=>{console.log("done")});
let tasks = Promise.resolve(10)
tasks = tasks.then(f);
tasks = tasks.then(f).then(f);
tasks = tasks.then(f).then(log);
tasks.then(terminate);
補足
処理開始の操作と順次増える処理の終了検知は、Async Funtionsが使えると解決する場合が多くあるだろう。
Async FunctionsについてはAsync Functionsという記事を書いたので、サンプルコードだけを示し、説明は委譲する。
function someAsyncCondition(val) {
return new Promise(done => done(val < 64));
}
function someAsyncTask(arg) {
return new Promise(done => setTimeout(acc => {
console.log(acc);
done(acc);
}, 100, arg));
}
async function func(){
let result = 1;
while(await someAsyncCondition(result)) {
result += await someAsyncTask(result);
}
return result;
}
func().then(result=>{console.log("done", result)});