JavaScript : Promise の挙動をおさらいする
普段は決まった書式で非同期処理を書いているのだが、Promise の仕様を押さえるため、思い付きで変な書式を試してみたりした。
今回紹介するサンプルコードは、全て promise-test.js
というファイル名で保存し、Node.js で実行した結果を確認している。
目次
- 同期的な処理は直接値を
return
すれば良い。 - 失敗例 : 非同期処理を Promise でラップできていない
- ある非同期処理が失敗しても後続の
then()
に繋げるには - 配列を直列で順に実行する Promise チェーンを作る
- 以上
同期的な処理は直接値を return
すれば良い。
then()
内の処理が同期的な処理であれば、きちんと待って次の then()
に移行してくれるので、
new Promise()
.then(() => {
return new Promise((resolve) => {
resolve(HOGE);
});
})
.then(() => {
return new Promise((resolve) => {
resolve(FUGA);
});
})
というように、全ての then()
内で new Promise()
を生成して return
しなくて良い。
new Promise()
.then(() => {
return HOGE;
})
.then(() => {
return HUGA;
});
同期的な処理であれば、コレで良いのだ。
以下により詳しいサンプルコードと実行結果を置く。
console.log('Start');
new Promise((resolve) => {
setTimeout(() => {
let val = 5;
resolve(val);
// resolve() 以降のコードも実行はされる
val += 20;
console.log('Promise 1 : ', val);
}, 1000);
})
.then((val) => {
console.log('Promise 2 : ', val);
// 同期的だが時間がかかる処理を用意する
let str = `${val} : `;
for(let i = 0; i < 10000000; i++) {
str += `${i}`;
}
return str.slice(-10);
})
.then((val) => {
console.log('Promise 3 : ', val);
});
$ node promise-test.js
Start
Promise 1 : 25
Promise 2 : 5
Promise 3 : 9989999999
$ node promise-test.js
# ↓ コマンド実行後すぐ出力される
Start
# ↓ setTimeout 内に書いてあるので1秒後に出力される
Promise 1 : 25
# resolve している値は 5 だが、console.log の値は += 20 した値
# ↓ 「Promise 1」の直後に出力される
Promise 2 : 5
# resolve している値は 5 なので、resolve 後に += 20 されていても無関係
# 時間のかかる for ループの処理が行われる
# then 内の関数が直接 return しているが、この値は Promise でラップされるので、次の then の仮引数で受け取れる
# ↓ 「Promise 2」の出力後、数秒開いて出力される
Promise 3 : 9989999999
# 時間のかかる for 文の処理を待って出力される
# 「Promise 2」の then 内の関数が return した str.slice(-10) が、仮引数 val で受け取れている
失敗例 : 非同期処理を Promise でラップできていない
then()
の中に非同期処理が含まれているのに、Promise でラップしていない場合は、後続の then()
に値を渡せない。
以下サンプル。
new Promise((resolve) => {
console.log('Promise 1');
resolve('Resolve!');
})
.then((val) => {
console.log('Promise 2 : ', val);
setTimeout(() => {
console.log('Promise 2 SetTimeout');
return 'Resolve?';
}, 1000);
})
.then((val) => {
console.log('Promise 3 : ', val);
});
$ node promise-test.js
Promise 1
Promise 2 : Resolve!
Promise 3 : undefined
Promise 2 SetTimeout
$ node promise-test.js
# ↓ コマンド実行直後に出力される
Promise 1
# ↓ 「Promise 1」の直後に出力される
Promise 2 : Resolve!
# 「Promise 1」は resolve で値を渡しているので「Resolve!」が引き継げている
# ↓ 「Promise 2」の直後、1秒待たずに出力される
Promise 3 : undefined
# setTimeout の処理を待てていないので、「Resolve?」の値が引き継げていない
# ↓ 「Promise 3」の約1秒後に出力される
Promise 2 SetTimeout
# 宙に浮いた非同期処理になっているので、Promise のチェーン全体が終わった後に出力されてしまっている
「Promise 3」が結果を受け取れず、変数 val
が undefined
になってしまっているのが分かる。
コレを修正するには、「Promise 2」部分で new Promise()
を return
するようにする。
new Promise((resolve) => {
console.log('Promise 1');
resolve('Resolve!');
})
.then((val) => {
// 「Promise オブジェクトを生成する関数」として作る
// then() の第1引数に直接 Promise オブジェクトを渡す、「then( new Promise() )」このような書き方は動かないので注意
// then() の第1引数を関数として実行してくるので、then(() => new Promise()) でないといけない
return new Promise((resolve) => {
console.log('Promise 2 : ', val);
setTimeout(() => {
console.log('Promise 2 SetTimeout');
resolve('Resolve?'); // return ではなく resolve で値を渡す
}, 1000);
});
})
.then((val) => {
console.log('Promise 3 : ', val);
});
$ node promise-test.js
Promise 1
Promise 2 : Resolve! # ← ココまではコマンド実行直後に出力される
Promise 2 SetTimeout # ← その1秒後に出力される
Promise 3 : Resolve? # ← SetTimeout の直後に出力され、「Resolve?」の文字列が取得できている
ある非同期処理が失敗しても後続の then()
に繋げるには
サンプルとして以下のようにランダムに Reject する関数を用意。
// 1秒後にランダムに Resolve か Reject する関数
function randomPromise(val) {
// resolve・reject の仮引数は好きな名前に変えても動く
return new Promise((res, rej) => {
setTimeout(() => {
if(Math.random() < 0.5) {
res(val || 'Default Resolve');
}
else {
rej(val || 'Default Reject');
}
}, 1000);
});
}
// この関数を3回呼び出してみる
console.log('Promise 1 : Start');
randomPromise('Promise 1 Result')
.then((val) => {
console.log('Promise 2 : ', val);
return randomPromise('Promise 2 Result');
})
.then((val) => {
console.log('Promise 3 : ', val);
return randomPromise('Promise 3 Result');
})
.catch((e) => {
console.error('Error! : ', e);
});
# 実行するたびに、どのタイミングで Reject されるかは変わる
$ node promise-test.js
Promise 1 : Start
Promise 2 : Promise 1 Result
Error! : Promise 2 Result
今回は、この randomPromise()
で仮に Reject されても、必ず後続処理に繋げる仕組みを作ってみる。
function randomPromise(val) {
return new Promise((res, rej) => {
setTimeout(() => {
if(Math.random() < 0.5) {
res(val || 'Default Resolve');
}
else {
rej(val || 'Default Reject');
}
}, 1000);
})
.then( // ← この then だけ追加、あとのコードは変えていない
(val) => {
return `${val} (Resolved)`;
},
(error) => { // then(onFulfilled, onRejected) と書けるのでこのように書く
console.log('Reject されたが続行 : ', error);
return `${error} (Rejected)`;
}
);
}
# 実行するたびに動作は変わるが、「Error!」は実行されない
$ node promise-test.js
Promise 1 : Start
# ↓ 1秒後に実行される
Promise 2 : Promise 1 Result (Resolved)
# 最初の randomPromise('Promise 1 Result') は Resovled した
# ↓ さらに1秒後に出力される
Reject されたが続行 : Promise 2 Result
# 「Promise 2」の randomPromise('Promise 2 Result') が Rejected したが、そのエラーをキャッチしている
# ↓ 直後に出力される
Promise 3 : Promise 2 Result (Rejected)
# return `${val} (Rejected)`; としたとおり、onRejected が return した値を受け取っている
このように、エラーを握り潰したい Promise オブジェクトの後ろに、空の onRejected
なチェーンを作っておけばよいので、以下のいずれかの構成が基本となるだろう。
// then(onFulfilled, onRejected) で書く場合
return new Promise((resolve, reject) => {
// 条件によって Resolve したり Reject したりする処理…
if( /* 条件 */ ) {
resolve();
}
else {
reject();
}
})
.then(
(value) => { return value; },
(error) => { return error; },
);
// もしくは、then().catch() と書く場合。結果は同じ
return new Promise((resolve, reject) => {
// 条件によって Resolve したり Reject したりする処理…
})
.then((value) => { return value; })
.catch((error) => { return error; });
それぞれの then()
・catch()
内でログだけ切り替えても良いし、return する値はエラー時のみ差し替えるなどしても良い。
配列を直列で順に実行する Promise チェーンを作る
Promise.all()
は配列で渡した非同期処理たちを並列で実行するが、直列で、順次実行したい場合は、Promise チェーンを作る。
// 適当な配列データ
const data = [
{ id: 1, name: '111' },
{ id: 2, name: '222' },
{ id: 3, name: '333' },
{ id: 4, name: '444' }
];
// Promise チェーンを開始する最初の Promise オブジェクトは、空の Promise.resolve() で作る
let promiseChain = Promise.resolve();
// 配列 data を順にループする
data.forEach((item) => {
// 変数 promiseChain に、promiseChain.then() と定義して代入を繰り返していく
promiseChain = promiseChain.then(() => new Promise((resolve, reject) => {
console.log(new Date(), item, 'Start');
setTimeout(resolve, item.id * 1000);
}));
});
// 最終的に promiseChain は、Promise.resolve().then(id: 1).then(id: 2).then(id: 3).then(id: 4) な Promise チェーンになっている
// 後から promiseChain に then() を繋げると、そこだけ後で実行される
setTimeout(() => {
promiseChain.then(() => {
console.log(new Date(), 'Finished');
});
}, 2000);
let promiseChain
で変数を用意し、data.forEach()
内で promiseChain = promiseChain.then();
と繋げていくのは、直感的で読解しやすいコードにはなるが、let
が登場するところがイマイチか。
そこで、Array.prototype.reduce
を使って同様の Promise チェーンを構築する方法も紹介する。コレが一番エレガントだと思います。
const promiseChain = data.reduce((prevPromise, item) => {
return prevPromise
.then(() => new Promise((resolve, reject) => {
console.log(new Date(), item, 'Start');
setTimeout(resolve, item.id * 1000);
}));
}, Promise.resolve());
setTimeout(() => {
promiseChain.then(() => {
console.log(new Date(), 'Finished');
});
}, 2000);
reduce()
は、第1引数の関数が return
した値を順に渡していく。また、第2引数に最初の値を指定できるので、まずは第2引数で Promise.resolve()
を用意する。
第1引数の関数にて、前回の関数が return
した値と今回のループで取り出した要素を取得できるので、return prevPromise.then();
とすることで Promise チェーンを実現する。
let promiseChain
で代入を繰り返した場合も、reduce()
で一気に構築した場合も、いずれも以下のような出力結果になる。
$ node promise-test.js
2018-08-15T07:35:40.345Z { id: 1, name: '111' } 'Start'
2018-08-15T07:35:41.354Z { id: 2, name: '222' } 'Start'
2018-08-15T07:35:43.360Z { id: 3, name: '333' } 'Start'
2018-08-15T07:35:46.366Z { id: 4, name: '444' } 'Start'
2018-08-15T07:35:50.372Z 'Finished'
以上
かなり Promise が身体に馴染んできた。そろそろ async・await も覚えねば…。