はじめに
JavaScriptの非同期処理を学び概念を理解してきたが、Promiseを作る側の関数(Promiseを返す関数)について疑問があったため、事象と結論を整理してみた。
【疑問】以下の2つの関数になぜ動作の違いが発生するのか?
①Promiseを返す関数として、SetTimeoutを「new Promise()(promiseのコンストラクター)」で定義した場合は、SetTimeoutの時間を待機した後Promiseを返す(これは検索するとよく出てくる一般的な内容)。
②Promiseを返す関数として、SetTimeoutを「async」のキーワードで括る定義をした場合は、setTimeoutの時間を待機せずにPromiseを返す。
この疑問を抱いた理由は、②のasyncで括ることでPromiseを返すことができるため、呼出し元(②の関数を呼び出す側の関数)で async/awaitを指定することでsetTimeoutの処理が待機されるのではと思ったが、期待する結果にはならなかった(待機されなかった)ため、疑問を持った。実際に動かしながら紐解いていく。
まずは2つの処理結果を比較してみる
①SetTimeoutを「new Promise()(promiseのコンストラクター)」で定義してPromiseを返す関数
// Promiseを返す関数
const delayedColorChange = (color, delay) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
document.body.style.backgroundColor = color;
console.log(`${color}に変えました。`);
resolve();
}, delay);
});
};
//Promise呼出し元の関数
async function rainbow() {
await delayedColorChange('red', 1000);
await delayedColorChange('Orange', 1000);
await delayedColorChange('yellow', 1000);
await delayedColorChange('green', 1000);
await delayedColorChange('blue', 1000);
await delayedColorChange('indigo', 1000);
await delayedColorChange('violet', 1000);
};
console.log('開始');
rainbow()
.then(() => console.log('終了'))
.catch((e) => console.log(e));
処理結果は、1秒毎に画面の色が変わっていき、想定通りSetTimeoutが待機される。
See the Pen new promise by minato (@Tminato) on CodePen.
②SetTimeoutを「async」のキーワードで括る定義をしてpromiseを返す関数
// Promise関数をAsync関数で定義
async function delayedColorChange(color, delay) {
setTimeout(() => {
document.body.style.backgroundColor = color;
console.log(`${color}に変えました。`);
}, delay)
return 'aaa'
};
//Promise呼出し元の関数
async function rainbow() {
await delayedColorChange('red', 1000);
await delayedColorChange('Orange', 1000);
await delayedColorChange('yellow', 1000);
await delayedColorChange('green', 1000);
await delayedColorChange('blue', 1000);
await delayedColorChange('indigo', 1000);
await delayedColorChange('violet', 1000);
};
console.log('開始');
rainbow()
.then(() => console.log('終了'))
.catch((e) => console.log(e));
処理結果は、①とは異なり同時に1秒後に全ての色が変わってしまい、結果として最後に設定した色になってしまう。
See the Pen Promise by minato (@Tminato) on CodePen.
動作の違いを調べてみる。
①SetTimeoutを「new Promise()(promiseのコンストラクター)」で定義してPromiseを返す関数
公式のドキュメントでPromiseの、コンストラクターを調べてみるとこんな記載があった。
executor については、以下のことを理解することが重要です。
・executor の返値は無視されます。
・executor の中でエラーが発生した場合、プロミスは拒否されます。つまり、 executor の中のコードが効果を発揮する仕組みは、次のようなものです。
・コンストラクターが新しい Promise オブジェクトを生成した時点で、対応する resolutionFunc と rejectionFunc の一対の関数も生成されます。これらは Promise オブジェクトに「結束」されます。
・executor 内のコードは、何らかの操作を行う機会を得、その結果を(値が他の Promise オブジェクトでない場合)「解決済み」または「拒否済み」として反映し、それぞれ resolutionFunc または rejectionFunc を呼び出して終了します。
・つまり、executor 内のコードは、 resolutionFunc や rejectionFunc による副次的影響を介して通信を行います。その副次的影響とは、 Promise オブジェクトが「解決済み」または「拒否済み」になることです。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
今回の①のコードの場合は、変数を以下のように読み替えることができ、もう少し簡潔に書くと
・executor → setTimeout
・resolutionFun → resolve
・rejectionFunc → reject
「promiseのコンストラクターはpromiseの生成時にsetTimeoutを実行し、そのsetTimeout内でresolveもしくはrejectが呼び出された時点でコンストラクターが終了する。」
と解釈することがでる。
そのため、setTimeoutが終了するまで、promiseのコンストラクターが終了するこはなく、promiseが戻らないため、結果として待機されるということにる。
検証として、①のソースで「resolve()」をコメントアウトして実行してみると、1つ目の赤色に変更するところで止まってしまい、Promiseのコンストラクターが終了していないことがわかる。
// Promiseを返す関数
const delayedColorChange = (color, delay) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
document.body.style.backgroundColor = color;
console.log(`${color}に変えました。`);
// resolve();
}, delay);
});
};
//Promise呼出し元の関数
async function rainbow() {
await delayedColorChange('red', 1000);
await delayedColorChange('Orange', 1000);
await delayedColorChange('yellow', 1000);
await delayedColorChange('green', 1000);
await delayedColorChange('blue', 1000);
await delayedColorChange('indigo', 1000);
await delayedColorChange('violet', 1000);
};
console.log('開始');
rainbow()
.then(() => console.log('終了'))
.catch((e) => console.log(e));
See the Pen promise resolve by minato (@Tminato) on CodePen.
②SetTimeoutを「async」のキーワードで括る定義をしてpromiseを返す関数
こちらについては、下記のリンク先の記事を参考にさせていただいた。
https://qiita.com/soarflat/items/1a9613e023200bbebcb3
returnされているときに promiseがresolve として戻り、
throw new Error()が行われたときに、rejectが戻ることがわかる。
そのため、
asyncのdelayedColorChange関数を呼び出した際に、setTimeoutの処理は実行されているが、setTimeoutの処理が完了するかを待つことはなく、次の処理のreturnに移ってpromiseのresolveが戻ることになる。
その結果、setTimeoutが7色分同時に処理され、最後の色のみに画面が変る結果になったということが分る。
SetTimeoutをasync/awaitで待機はできない...
前述で、asnycでsetTimeoutの処理を待たずに、promiseを返す関数に違いがあることが分かった。
setTimeoutを待機するために、 awaitキーワードをつけてあげれば待機できるのではと考え、これを試してみる。
// Promise関数をAsync関数で定義
async function delayedColorChange(color, delay) {
await setTimeout(() => {
document.body.style.backgroundColor = color;
console.log(`${color}に変えました。`);
}, delay)
return 'aaa'
};
//Promise呼出し元の関数
async function rainbow() {
await delayedColorChange('red', 1000);
await delayedColorChange('Orange', 1000);
await delayedColorChange('yellow', 1000);
await delayedColorChange('green', 1000);
await delayedColorChange('blue', 1000);
await delayedColorChange('indigo', 1000);
await delayedColorChange('violet', 1000);
};
console.log('開始');
rainbow()
.then(() => console.log('終了'))
.catch((e) => console.log(e));
結果は意味なし...
これは、awaitすることができるのは、promiseを返す関数のみであり、setTimeoutは非同期処理であるが、promiseを返さないためawaitを利用することができない。
そうすると、awaitを記載するには、setTimeoutをpromiseを返す関数にする必要がり、以下のようなコードとなる。
// Promis関数をAsync/awaitで定義
async function delayedColorChange(color, delay) {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
document.body.style.backgroundColor = color;
console.log(`${color}に変えました。`);
resolve();
}, delay)
})
return await p1
};
//Promise呼出し元の関数
async function rainbow() {
await delayedColorChange('red', 1000);
await delayedColorChange('Orange', 1000);
await delayedColorChange('yellow', 1000);
await delayedColorChange('green', 1000);
await delayedColorChange('blue', 1000);
await delayedColorChange('indigo', 1000);
await delayedColorChange('violet', 1000);
};
console.log('開始');
rainbow()
.then(() => console.log('終了'))
.catch((e) => console.log(e));
See the Pen Primise async awit by minato (@Tminato) on CodePen.
1秒ごとに色が変わり、想定する結果が得られることが分かる。
ただし、これは結局のところ、①SetTimeoutを「new Promise()(promiseのコンストラクター)」で定義したものに対して
awaitをかけているだけであり、①のSetTimeoutを「new Promise()(promiseのコンストラクター)」で定義してPromiseを返す関数と同じであると考えられる。
まとめ
ということで、結論。
promiseを返す関数を定義する際に、setTimeout(非同期処理であるがpromiseを返さない関数)については、promiseのコンストラクターで定義することで処理を待機させることはできる。一方でasyncで定義した関数の場合は、setTimeout処理をasync関数内で待機させることができないということが分かった。理由は、setTimeoutがpromiseを戻り値としないため、async関数内でawaitすることができず、setTimeoutの関数の実行のみされて次の処理(return)へ進んでしまうためだ。結果として、呼び出し元でawaitをかけたところで、promiseを返す関数自体はsetTimeoutを実行のみして終了してしまうため、setTimeoutの待機時間に関係なく処理が進んでしまうという結論に至った。