LoginSignup
3
2

More than 1 year has passed since last update.

SetTimeoutは、async/await関数では使えない

Last updated at Posted at 2022-09-29

はじめに

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 オブジェクトが「解決済み」または「拒否済み」になることです。

:link: 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を返す関数

こちらについては、下記のリンク先の記事を参考にさせていただいた。
:link: 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));

image.png
結果は意味なし...
これは、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の待機時間に関係なく処理が進んでしまうという結論に至った。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2