質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.04%

javascriptのasync/awaitの挙動の得体・仕組みが分からない

解決済

回答 3

投稿

  • 評価
  • クリップ 7
  • VIEW 814

ganariya2525

score 45

前提・実現したいこと

javascriptのasync/awaitを使用するプログラムを理解する必要があり、色々とプログラムを動かしていたのですが、自分と考えている挙動と異なるasync, awaitの挙動が有り、どうしてそうなるのか分かっていません。

試したこと・質問1

function asyncFunc() {
    return new Promise((resolve => {
        console.log(3);
        resolve("he");
    }));
}

async function wait() {
    console.log(2);
    let a = await asyncFunc().then((val) => {
        console.log(val);
        return val + " likes";
    });
    console.log(a);
    console.log(5);
}

console.log(1);
wait();
console.log(4);

以上のコードを実行すると

1
2
3
4
he
helikes
5

のようになりました。

これが起きる理由として

  1. 1がコンソールへ
  2. wait関数がスタックに積まれる
  3. 2がコンソールへ
  4. asyncFuncの実行でスタックに積む
  5. 3がコンソールへ
  6. resolve("he")をして、PromisがFulfill状態になる。
  7. await asyncFunc()のようになっているため、強制的にこのwait関数のasync移行はキューに積まれる。
  8. wait関数はキューに写ったので、4をコンソールへ。
  9. スタックがからのため、キューに戻ってくる。
  10. asyncFuncの返り値のFulFilで設定したconsole.valを実行(he)
  11. return val + "likes"で、"he likes"オブジェクトがPromisenoFulFil状態で帰る。
  12. aのawaitが終了し、he likesとなり、コンソールへ
  13. 最後に5を出力する。

のように考えています。

ここで、考えた私の仮説として

async関数内でawaitを使用すると、await内でなにか非同期処理が生じた瞬間、それ以降の内容は強制的にキューにぶち込み、スタックが空になってから実行する。
上記のルールがあるため、3を出力してresolveをした後(resolveは非同期処理のため)、これら以降のwait関数の処理を飛ばし、先に外側の4を出力しているのだと考えました。

質問として、上記のルールは正しいのでしょうか?

試したこと・質問2

2はかなり非同期処理の挙動が複雑になっています。

async function asyncFunction(time) {
    console.log("asyncFuntion" + time)
    return time;
}

async function hoge() {
    return 0;
}

async function hen(name) {
    let j = 0.99;
    console.log("nyan " + name);
    let c = await hoge();
    console.log("in hen " + name);
     c = await hoge();
    for (let i = 0; i <= 100000000; i++) j *= 0.99;
    console.log("out hen " + name);
    return j;
}

async function rapper() {

    // awaitはそれ移行全部キューに入れる
    let arr = [await asyncFunction(0).then((val) => {
        return val * 20
    }), asyncFunction(1)];

    console.log(await Promise.all(arr))
}

rapper();
console.log("here");
console.log(hen("global"));
console.log("nannde");

以上のコードを実行したところ

asyncFuntion0
here
nyan global
Promise {pending}
nannde
in hen global
asyncFuntion1
out hen global
(2) [0, 1]

のようになりました。

これの挙動は

  1. rapper関数を実行する
  2. let arr内のawait asyncFunctionを実行する
  3. コンソールにasyncFunction 0
  4. awaitが実行されたので、強制的にこれ以降(awaitFunction(0)以降)を、スタックからキューに写す。
  5. コンソールにhere
  6. hen("global")を実行する
  7. nyan globalをコンソールに
  8. await hogeをする。ここで、awaitが実行されたので、強制的にこれ以降(let c = await hoge()以降)を、スタックからキューに写す。
  9. グローバルに戻ってきて、コンソールにhen("global")のPending状態のPromiseを出力する。
  10. nanndeをコンソールに出力する
  11. ここでどうしてかhen関数内に戻る(キューの追加順を考えれば、rapperに戻らないとおかしい。)
  12. in hen globalを出力する
  13. await hogeをおこない、強制的に抜け出る。
  14. キューからrapperを取り出して、asyncFunction(1)を実行し、asyncFunction1を実行する
  15. await Promise.allをする。よって、強制的に抜け出る。
  16. for(let i = 0;i<=10000000;i++)の重い処理が同期処理される。
  17. out hen globalを出力する。
  18. 最後に(2)[0, 1]を出力する。

であると考えています。

ここで気になった質問なのですが

2-1. awaitキーワードがあった時点で内部関数を実行し、その内部関数で非同期処理の内容があった瞬間、それ以降をすべてキューに移し、他のスタック処理を行うという認識はあっていますでしょうか?(挙動の4, 8などです)
2-2. 11でhen関数内に戻っていて、キューの追加順を考えると、hen関数内に戻るのはおかしいと考えているのですが、これはキューに追加されていないということなのでしょうか?
2-3. async, await, setTimeoutの挙動は同じなのでしょうか?(非同期処理という意味で一概に同じグループにして大丈夫でしょうか)

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

+4

async, await, setTimeoutの挙動は同じなのでしょうか?

「promise、await」と「setTimeout」は異なる挙動をします。なぜならそれぞれ異なるキューが用意されていて、実行のされ方も異なるからです。

非同期に呼び出される関数が蓄えられるキューは2種類あり、task queue と microtask queue です。promise のコールバックと MutationObserver のコールバックは microtask queue に入れられ、それ以外の非同期なコールバック(setTimeout など)はすべて task queue に入れられます。(queue に入れられるタイミングはコールバックがセットされたタイミングではなくて、呼び出されほしい、という時です。例えば Promise ならそれが resolve や reject したタイミングです)。

注意: requestAnimationFrame()のコールバックは別のタイミングで実行されます。

あまり細かいことは自分も分かっていませんが、基本的にコールスタックが空になるとすぐに microtask checkpoint というのが実行されて、microtask queue に溜まっている microtask が古い順にすべて実行されます。もしその microtask が(例えば Promise.resolve().then(...) などで)即座に別の microtask をエンキューしたとしても、それも含めて microtask queue が空になるまで実行されます。

一方、task は一つづつしか実行されません。task queue から一番古い task が一つ取り出され、それを実行し終わるとすぐに microtask checkpoint が実行され、そのあと画面のレンダリングが行われ(行われないこともある)、これが繰り返されます。

JavaScript の実行は event loop というもので制御されており、これが上で述べたような task queue や microtask queue の実行、レンダリングなどを順に繰り返し実行するようになっていて、詳しくは 8.1.4.3 Processing model で定義されています。

いろいろ説明してきましたが、このプレゼンテーション動画が非常にわかりやすいのでおすすめです。英語ですが、字幕もついています。

以上を踏まえると、例えば下のコードの実行結果はさらにその下のようになります。

!(async () => {
  setTimeout(() => {
    console.log("setTimeout1")
    Promise.resolve("promise3").then(console.log)
  })
  setTimeout(() => console.log("setTimeout2"))
  Promise.resolve("promise1").then(console.log)
  console.log(await "await 1")
  Promise.resolve("promise2").then(console.log)
  console.log(await "await 2")
})()
console.log("1")
1
promise1
await 1
promise2
await 2
setTimeout1
promise3
setTimeout2

DEMO(JSFiddle)

await する値が promise でなくともそれ以降の処理は microtask となり、振る舞いとしては Promise.resolve() と同等であることが分かると思います。また、microtask が task より前に実行されていることも観察できます。microtask promise3 は task setTimeout1 が終了した直後かつ、task setTimeout2 より前に実行されています。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+2

試したこと・質問1

質問として、上記のルールは正しいのでしょうか? 

正しいのではないかと思います。

試したこと・質問2

awaitキーワードがあった時点で内部関数を実行し、その内部関数で非同期処理の内容があった瞬間、それ以降をすべてキューに移し、他のスタック処理を行うという認識はあっていますでしょうか?(挙動の4, 8などです)

合っていると思います。

11でhen関数内に戻っていて、キューの追加順を考えると、hen関数内に戻るのはおかしいと考えているのですが、これはキューに追加されていないということなのでしょうか?

これ、かなり悩まされました。勉強になりました、ありがとうございます。

私見ですが、asyncFunction(0)が返すpromiseは、hen関数内よりも先に処理されているのだと思います。ただ、asyncFunction(0).then()が返すpromiseは、hen関数内よりも後なのだと思います。

ご提示のコードを変更して、

    let arr = [await asyncFunction(0).then((val) => {
        console.log('asyncFunction(0).then()');
        return val * 20
    }), asyncFunction(1)];


としてみると、わかりやすいのではないでしょうか。

また、この考えが正しいのかを検証するために、以下のテストコードを書いてみました。

async function mainAsyncFunction() {
  console.log('mainAsyncFuntion');
  await Promise.resolve().then(()=>{
    console.log('mainAsyncFuntion.then()');
  }).then(()=>{
    console.log('mainAsyncFuntion.then().then()');
  }).then(()=>{
    console.log('mainAsyncFuntion.then().then().then()');
  });
}

async function subAsyncFunction() {
  console.log('subAsyncFunction');
  await wait();
  console.log('subAsyncFunction');
  await wait();
  console.log('subAsyncFunction');
}

async function wait(){}

mainAsyncFunction();
subAsyncFunction();

/*
mainAsyncFuntion
subAsyncFunction
mainAsyncFuntion.then()
subAsyncFunction
mainAsyncFuntion.then().then()
subAsyncFunction
mainAsyncFuntion.then().then().then()
*/

async, await, setTimeoutの挙動は同じなのでしょうか?(非同期処理という意味で一概に同じグループにして大丈夫でしょうか)

async, await そして Promise については同じと見てよさそうです。

Jobs and Job Queues | ECMAScript® 2017 Language Specification

setTimeoutについては、Jobs and Job Queuesとの関係性がよくわかりませんでした。他の回答者様、よろしくお願いします。

Microtask queuing | HTML Standard
Event loops | HTML Standard

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/09/19 21:30

    ありがとうございます!
    なんとなくawaitの挙動がわかりました。
    awaitは非同期処理の挙動となったとき、resolve()のキューに積むのと同じようにキューに積むのですね・・・
    勉強になります!
    setTimeoutが絡むコードを色々ためすとかなり複雑でよく分かってないのでもう少し考えてみようと思います!

    キャンセル

0

こう書いたほうがわかりやすいのでは?

const asyncFunc=()=>{
  return new Promise(resolve=>{
    setTimeout(()=>{
      console.log(3);
      resolve("he");
    },1000);
  });
}

const wait=async()=>{
    console.log(2);
    let a = await asyncFunc().then(val=>{
      console.log(val);
      return val + " likes";
    });
    console.log(a);
    console.log(5);
}

console.log(1);
wait();
console.log(4);


結果:
1
2
4
(1秒待って)
3
he
he likes
5

これが、waitをasyncしないと

const asyncFunc=()=>{
  return new Promise(resolve=>{
    setTimeout(()=>{
      console.log(3);
      resolve("he");
    },1000);
  });
}

const wait=()=>{
    console.log(2);
    let a = asyncFunc().then(val=>{
      console.log(val);
      return val + " likes";
    });
    console.log(a);
    console.log(5);
}

console.log(1);
wait();
console.log(4);


結果:
1
2
Promise { <state>: "pending" }
5
4
(1秒待って)
3
he
※すでにasnycFuncが処理済みなのでhe likesは表示されない

さらに

const asyncFunc=()=>{
  return new Promise(resolve=>{
    setTimeout(()=>{
      console.log(3);
      resolve("he");
    },1000);
  });
}

const wait=async()=>{
    console.log(2.1);
    console.log(2.2);
    await console.log(2.3);
    console.log(2.4);
    console.log(2.5);
    let a = await asyncFunc().then(val=>{
      console.log(val);
      return val + " likes";
    });
    console.log(a);
    console.log(5);
}

console.log(1);
wait();
console.log(4.1);
for(var i=0;i<999999999;i++){} //同期処理で重めのループ
console.log(4.2);


結果:
1
2.1
2.2
2.3 //ここでawaitがかかるので処理が移り
4.1
(ちょっと考え込んで)
4.2 //同期処理が続くので終了まで待ち
2.4 //元の処理にもど
2.5
(1秒待って)
3
he
he likes
5

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/09/19 19:28

    もうすこし突っ込んだ処理を追記しておきました

    キャンセル

  • 2019/09/19 21:39

    ```javascript
    const asyncFunc = () => {
    return new Promise(resolve => {
    setTimeout(() => {
    console.log(3);
    resolve("he");
    }, 1000);
    });
    }

    const func = async () => {
    return 10;
    }

    const asyncTwo = async () => {
    console.log(10);
    console.log(11);
    await new Promise((resolve, reject)=>{
    resolve();
    }).then((val)=>{
    console.log("log")
    })
    console.log(12);
    console.log(13);
    };

    const wait = async () => {
    console.log(2.1);
    console.log(2.2);
    await asyncTwo();
    console.log(2.4);
    console.log(2.5);
    let a = await asyncFunc().then(val => {
    console.log(val);
    return val + " likes";
    });
    console.log(a);
    console.log(5);
    }

    console.log(1);
    wait();
    console.log(4.1);
    for (var i = 0; i < 999999999; i++) {} //同期処理で重めのループ
    console.log(4.2);
    ```

    ```
    const asyncFunc = () => {
    return new Promise(resolve => {
    setTimeout(() => {
    console.log(3);
    resolve("he");
    }, 1000);
    });
    }

    const func = async () => {
    return 10;
    }

    const asyncTwo = async () => {
    console.log(10);
    console.log(11);
    new Promise((resolve, reject)=>{
    resolve();
    }).then((val)=>{
    console.log("log")
    })
    console.log(12);
    console.log(13);
    };

    const wait = async () => {
    console.log(2.1);
    console.log(2.2);
    await asyncTwo();
    console.log(2.4);
    console.log(2.5);
    let a = await asyncFunc().then(val => {
    console.log(val);
    return val + " likes";
    });
    console.log(a);
    console.log(5);
    }

    console.log(1);
    wait();
    console.log(4.1);
    for (var i = 0; i < 999999999; i++) {} //同期処理で重めのループ
    console.log(4.2);
    ```
    だと結果が違うのですね・・・
    awaitだと強制的にキューに突っ込むことがなんとなく分かりました!
    (new Promiseはただの実行でthen内だけ後回しでキューなのですね)
    ありがとうございます!

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.04%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる