🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

Q&A

解決済

3回答

1793閲覧

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

ganariya

総合スコア50

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

2グッド

9クリップ

投稿2019/09/19 06:58

前提・実現したいこと

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

試したこと・質問1

javascript

1function asyncFunc() { 2 return new Promise((resolve => { 3 console.log(3); 4 resolve("he"); 5 })); 6} 7 8async function wait() { 9 console.log(2); 10 let a = await asyncFunc().then((val) => { 11 console.log(val); 12 return val + " likes"; 13 }); 14 console.log(a); 15 console.log(5); 16} 17 18console.log(1); 19wait(); 20console.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の挙動は同じなのでしょうか?(非同期処理という意味で一概に同じグループにして大丈夫でしょうか)

Lhankor_Mhy, yohhoy👍を押しています

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

ベストアンサー

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 で定義されています。

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

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

js

1!(async () => { 2 setTimeout(() => { 3 console.log("setTimeout1") 4 Promise.resolve("promise3").then(console.log) 5 }) 6 setTimeout(() => console.log("setTimeout2")) 7 Promise.resolve("promise1").then(console.log) 8 console.log(await "await 1") 9 Promise.resolve("promise2").then(console.log) 10 console.log(await "await 2") 11})() 12console.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 より前に実行されています。

投稿2019/09/20 08:55

編集2019/09/20 09:07
karamarimo

総合スコア2553

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

試したこと・質問1

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

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

試したこと・質問2

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

合っていると思います。

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

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

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

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

js

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

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

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

js

1async function mainAsyncFunction() { 2 console.log('mainAsyncFuntion'); 3 await Promise.resolve().then(()=>{ 4 console.log('mainAsyncFuntion.then()'); 5 }).then(()=>{ 6 console.log('mainAsyncFuntion.then().then()'); 7 }).then(()=>{ 8 console.log('mainAsyncFuntion.then().then().then()'); 9 }); 10} 11 12async function subAsyncFunction() { 13 console.log('subAsyncFunction'); 14 await wait(); 15 console.log('subAsyncFunction'); 16 await wait(); 17 console.log('subAsyncFunction'); 18} 19 20async function wait(){} 21 22mainAsyncFunction(); 23subAsyncFunction(); 24 25/* 26mainAsyncFuntion 27subAsyncFunction 28mainAsyncFuntion.then() 29subAsyncFunction 30mainAsyncFuntion.then().then() 31subAsyncFunction 32mainAsyncFuntion.then().then().then() 33*/ 34

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 10:06

編集2019/09/19 10:09
Lhankor_Mhy

総合スコア36928

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ganariya

2019/09/19 12:30

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

0

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

javascript

1const asyncFunc=()=>{ 2 return new Promise(resolve=>{ 3 setTimeout(()=>{ 4 console.log(3); 5 resolve("he"); 6 },1000); 7 }); 8} 9 10const wait=async()=>{ 11 console.log(2); 12 let a = await asyncFunc().then(val=>{ 13 console.log(val); 14 return val + " likes"; 15 }); 16 console.log(a); 17 console.log(5); 18} 19 20console.log(1); 21wait(); 22console.log(4);

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

これが、waitをasyncしないと

javascript

1const asyncFunc=()=>{ 2 return new Promise(resolve=>{ 3 setTimeout(()=>{ 4 console.log(3); 5 resolve("he"); 6 },1000); 7 }); 8} 9 10const wait=()=>{ 11 console.log(2); 12 let a = asyncFunc().then(val=>{ 13 console.log(val); 14 return val + " likes"; 15 }); 16 console.log(a); 17 console.log(5); 18} 19 20console.log(1); 21wait(); 22console.log(4);

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

さらに

javascript

1const asyncFunc=()=>{ 2 return new Promise(resolve=>{ 3 setTimeout(()=>{ 4 console.log(3); 5 resolve("he"); 6 },1000); 7 }); 8} 9 10const wait=async()=>{ 11 console.log(2.1); 12 console.log(2.2); 13 await console.log(2.3); 14 console.log(2.4); 15 console.log(2.5); 16 let a = await asyncFunc().then(val=>{ 17 console.log(val); 18 return val + " likes"; 19 }); 20 console.log(a); 21 console.log(5); 22} 23 24console.log(1); 25wait(); 26console.log(4.1); 27for(var i=0;i<999999999;i++){} //同期処理で重めのループ 28console.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 07:49

編集2019/09/19 10:21
yambejp

総合スコア116661

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yambejp

2019/09/19 10:28

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

2019/09/19 12: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内だけ後回しでキューなのですね) ありがとうございます!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問