MENU

JavaScript同期・非同期構文

【JavaScript】 async/awaitの処理の流れを図で解説します

更新日:2021/01/07

 

 

JavaScriptのasyncとawaitというキーワードは、非同期処理を簡潔に記述することができます。

 

しかしプログラムの流れを変えてしまう面もあり、理解するのが難しい機能でもあります。

 

そこで今回はコード上での流れを中心に、asyncとawaitについて解説します。

 

async/awaitの概要

 

Promiseの解説記事で「Promiseは処理を他に丸投げして、自分の処理を続行できる機能」と説明しました。

 

async/awaitは、丸投げされた側の対応になります。

 

Promiseの解説記事と同じ手法で、まずは実際の仕事におきかえてみます。

 

 

この仕事、今すぐでなくてもいいんだよな…

 

他の仕事もあるし…

 

そうだ、外部の業者に丸投げしよう!!


 

 

僕が報告を受けるのも面倒だから、受付担当を作っておこうかな…


 

resolve  reject

 

そしてお仕事を発注します

 

 

 

async社さん、この内容でお仕事お願いしますね

 


 

 

 

受けたわまりました!
我がasync社は、完璧に仕事を終わらせます!!

 

で、納期はいつまでに・・・?

 


 

 

 

特にないので終わったらでいいですよー

 

終わったら受付の resolveさんに報告してくださいねー

 

何か問題があったら rejectさんにお願いしますー

 


 

 

 

わかりました!

 


 

 

 

ではそのような約束(Promise)でお願いしますー

 


 

 

そして・・・

 

 

 

急ぎの仕事だけやっていればいいから、はかどるなー


 

残った仕事をバリバリ処理していくのであった

 

 

 

一方async社さんは・・・

 

 

 

 

あー疲れた。少し休憩したいな…

 

でも受注した仕事を少しでも早く終わらせたいな…

 

少し他の人に頼んで、その間に休憩しよう

 

awaitさんー、いますかー?

 


 

 

はーい!なんですかー?


 

 

 

ちょっと休憩したいので、その間にこの仕事やってもらえますか?

 


 

 

はーい!よろこんでー!!


 

休憩後・・・

 

 

 

awaitさん、まだ作業中かな?

 

終わるまで待っていよう・・・

 


 

 

終わりましたー!


 

 

 

ありがとう。

 

続きは僕がやるよ。

 


 

つまりasync内で処理をawaitに丸投げした場合、丸投げしている間は処理を中断できるのです。

asyncとは

 

関数定義の前にasyncを付加すると、非同期関数オブジェクトが生成されます。
非同期関数を実行すると暗黙的にPromiseオブジェクトを返します。

 

そのためイメージ的にはPromiseオブジェクトを簡易的に作成する糖衣構文です。

 

asyncとPromise

 

例えば、次のようなPromiseを返すだけの関数があるとします。

 

Promiseを返す関数

 


function b(){
    
    return new Promise( (resolve, reject) =>{

        const x = 2;
        
        resolve( x * 2 );
        
    });
}

 

この関数をasyncを使用して書き換えてみます。

 

Promiseを返す関数をasyncに変更

 


async function b(){

  const x = 2;

  return x * 2;
  
}

 

new Promiseに渡している関数を、そのまま記述しています。
ただしresolveで指定していた値は、asyncではreturnで指定します。
しかしrejectは利用できないので、throwを利用するか、resolveでエラー値を渡すなどの工夫が必要です。

 

asyncはPromiseオブジェクトを簡易的に作成する糖衣構文

 

=>asyncキーワードが付加された関数は、評価時にコードがreturn new Promiseでラップされ、return値がresolveでラップされる。

 

このようにasyncを使用すると、関数内でPromiseを使用しなくても自動的に一連の処理をおこなってくれるのです。

 

asyncを適用できない例

 

ただし次のように、クロージャの特性を活かすためなどの理由で、Promiseを返す前になんらかの処理をおこなっているコードはasyncに書き換えることができません。

 

asyncを適用できない例

 


function b(){
    
    const y = 3;

    return new Promise( (resolve, reject) =>{

        const x = 2;
        
        resolve( x * y );
        
    });
}

 

またコールバック関数内でPromiseを解決する場合、asyncを適用できません。

 

asyncを適用できない例

 

function b(){
    
    return new Promise( (resolve, reject) =>{

        btn.addEventListener("click", ()=>{
            resolve( true );
       });
        
    });
}

 

addEventListenerで指定したコールバック関数は、Promiseを生成した関数とは別のものです。
そのためコールバック関数内でreturnしても、意味がありません。

 

asyncを適用できない例

 


async function b(){
   
    btn.addEventListener("click", ()=>{
            return  true;  // コールバック関数内でreturnしてもresolveにはならない
    });

    // returnがないので、resolve( undefined )が適用される
}

 

上の例では、結果がundefinedになります。

 

asyncとthen()・catch()

 

またasyncもPromiseオブジェクトを返すので、呼び出し元でthen()やcatch()を利用できます。

 

呼び出し側でのthen()、catch()利用

 


b().then( e=>{ ・・・処理 } ).catch( e=>{ ・・・処理 } ) ;

 

asyncイコールPromiseと覚えておけば間違いありません。

 

asyncの使い方

 

asyncは関数定義や関数式で使用できます。

 

asyncの使い方

 


async function b(){ }  // 関数定義

const b = async function (){ };  // 関数式

const b = async  ()=>{ };    // アロー関数

awaitとは

 

awaitキーワードは、関数を実行するときに、関数名の前に付加して使用します。
ただし、いくつかの条件があります。

 

awaitキーワードはasyncキーワード内のみ使用可能

 

awaitはasyncキーワードが付加された関数、つまり非同期関数内でのみ使用可能です。

 

awaitはasync内でのみ使用可能

 

const b = async  ()=>{
    
    const x = 2;

    const r = await c();
    
    return x * r;
}

 

asyncはイメージ的には、return new Promiseの糖衣構文ですが、そちらでは使用できません。

 

awaitはasync内でのみ使用可能

 

const b =  ()=>{
    
    return new Promise( (resolve, reject) =>{
        const x = 2;

        const r = await c(); // エラー!!
    
        return x * r;
    });
}

 

awaitキーワードはPromiseオブジェクトを返す関数のみ使用可能

 

awaitキーワードはPromiseオブジェクトを返す関数と非同期関数のみ使用可能です。

 

 

awaitキーワードの使い方

 

async function b(){ }

async function c(){

   await b();

}

 

呼び出し元のasyncもPromiseを生成するので、awaitを使用するとPromiseが入れ子になります。
複雑な状況ですが、最初に紹介した流れ図を念頭に置いておけば、それほど難しくありません。

 

awaitキーワードが出現した時点で呼び出し元に処理が戻る

 

async内では上から順番にコードが処理されます。

 

しかしawaitキーワードが付加された関数が実行されると、その時点で呼び出し元に処理が戻ります。

 

awaitキーワードの使い方

 

async function b(){
    return 3;
}

async function c(){

   const x = 2;

   const r = await b(); // b()処理後、aに戻る
   
   const y = 4;
   return r * x * y;
}
function a(){

     c().then( e => console.log( e ) );

}

 

上の例では、関数c内で関数bを実行した後、即座に関数aに戻ります。

 

コードが続いているのに処理が終わってしまうのは、これまでのプログラムの常識から外れていますね。

 

イメージしては、関数cは次のように書き換えることができます。

 

awaitキーワードを書き換え

 

async function b(){
    return 3;
}
function c(){
  return new Promise( (resolve, reject) =>{
     const x = 2;

     b().then( r => {

         const y = 4;

         resolve( r * x * y );

       } );
  } );
}
function a(){

     c().then( e => console.log( e ) );

}

 

Promiseを理解している人は、こちらの形式なら納得できると思います。
難しく考えずに、こういうものだと思っておくと幸せになります。

 

なおawaitと同じように、処理途中で呼び出し元の関数に戻ってしまうyieldというキーワードがあります。
知らないって人は、一度みてください。
参考記事:【JavaScript】 Generator(ジェネレーター)とyieldってなんだ?関数とは何が違うのか

 

awaitの結果は戻り値で取得する

 

await関数内でresolve()が呼び出されると、resolve()の引数がawaitの結果として戻ります。

 

awaitの結果を取得

 

async function b(){
    return 3; // resolve(3)と同値
}

async function c(){

   const x = 2;

   const r = await b(); // b()処理後、aに戻る(メインの流れ終了)
                               // b()でresolve(3)が呼ばれると、rに3がセットされる
   const y = 4;
   return r * x * y;
}

 

ただしawait関数内でreject()またはthrowされると、エラーとなります。
処理を止めないためには、catch()で捕捉します。

 

awaitの結果がrejectの場合

 

function b(){
   return new Promise( (resolve, reject) =>{
      reject("error!!");
   });
}

async function c(){

   const x = 2;

   const r = await b().catch( e=>console.log( e ) );  // コンソール:error!!
   
   console.log( r );  // コンソール:undefined

   const y = 4;
   return r * x * y;
}

 

しかし、then()と同様にawait以降のコードも処理されます。
その際にawaitの戻り値がundefinedになります。

 

そのため戻り値のチェックが必要ですが、undefinedはコードに不具合があることでセットされるケースもあります。
できればresolve()を使用して、仕様としてエラーと決めた値をセットするべきです。

 

エラー時にrejectを使用しない

 

function b(){
   return new Promise( (resolve, reject) =>{
      // エラーだったらnullをresolve()する
      resolve(null);
   });
}

async function c(){

   const x = 2;

   const r = await b();
   
   if( r === null ) { // エラーかどうかチェック
      console.log( "error!!");
      r = 1;
   }

   const y = 4;
   return r * x * y;
}

 

 

awaitは複数指定できる

 

一つのasync関数内で、awaitキーワードは何度でも使用することができます。

 

awaitの複数回使用

 


async c(){

   const x = await b(); // b()処理後、呼び出し元に戻る
   const r = await b();
   const y = await b();

   return r * x * y;
}

 

このとき呼び出し元に処理が戻るのは、一回目のawait関数実行後です。
二回目以降で処理が戻ることはありません。

 

async/awaitの使用例

 

async/awaitを使用した、簡単な例を挙げてみます。

 

実行例

 

『アンケートに答える』を押してアンケートにご協力ください。

 

 

html

 


『アンケートに答える』を押してアンケートにご協力ください。
<button id="b1"  >アンケートに答える</button>
<div id="div1" style="display: none">
    <p>あなたの性別は?</p>
    <p><label><input type="radio" name="seibetu"  value="男" checked>男</label>
        <label><input type="radio" name="seibetu"  value="女">女</label></p>
    <p><button id="b2">決定</button><button id="b3">キャンセル</button></p>
</div>

 

JavaScript

 


window.addEventListener( "DOMContentLoaded" , ()=> {
    const btn = document.getElementById("b1");
    const div = document.getElementById("div1");

    /**
     * アンケート答えるボタンクリックイベント
     */
    btn.addEventListener("click",()=>{
        btn.disabled = true;
        div.style.display="block";
        questionnaire().then(e=>{
            alert(e);
            div.style.display="none";
            btn.disabled = false;
        });
    });

    /**
     * アンケート受付処理
     * @returns {Promise}
     */
    async function questionnaire(){
        const res = await decision();
        return res === null ? "キャンセルしました" : `あなたは${res}です`;
    }

    /**
     * 決定・キャンセルボタンクリック待ち
     * @returns {Promise}
     */
    function decision() {

        return new Promise( (resolve)=>{
            const b2 = document.getElementById("b2");
            const b3 = document.getElementById("b3");
            const click = function(e){
                b2.removeEventListener("click",click);
                b3.removeEventListener("click",click);
                if( e.target.id === "b3" ) resolve(null);
                else{
                    const s = document.querySelectorAll("input[name='seibetu']");
                    if( s[0].checked ) resolve(s[0].value);
                    else resolve(s[1].value);
                }
            };

            b2.addEventListener("click",click);
            b3.addEventListener("click",click);
        });

    }
});

 

アンケートといっても、ここでおこなっていることは次の二つだけです。

 

(1) 『アンケートに答える』ボタンが押されたら、アンケートを表示してアンケートの結果が出るまでボタンを無効にする
(2) 決定またはキャンセルボタンが押されたら結果をアラート表示して、アンケートを消し、アンケートの結果が出るまでボタンを有効にする

 

ここでのポイントは、アンケートの処理をasync関数内に閉じ込め、『アンケートに答える』ボタンのイベント内では、ボタンの有効無効およびアンケート画面の表示非表示の切り替えだけをおこなっている点です。

補足:asyncとawaitの処理の流れ

 

ネットでasyncとawaitについて調べると、『awaitは処理を一時停止する』または『待機する』などと表現されているケースが多いです。

 

この表現は勘違いの元なので、asyncとawaitの解説をする前に処理の流れを簡単に図でお伝えします。

 

よくある間違い

 

次のコードは、何らかの処理の途中でブラウザ上のボタンが押されるのを待ち、ボタンが押されたら続きの処理をおこなうことを想定したコードです。

 

『awaitは処理を一時停止する』なら、上手く動くはずです。

 


function a(){

    // 何らかの処理

   b().then( e => console.log(e) ); // ボタンが押されるまで待ちたい

   // 何らかの処理

}

// awaitはasync内で使用すると聞いたので作成した関数
async function b(){ 

    const r = await c(); // 関数cが終了するまで待つ

    console.log( r );

    return "b end";
}
function c(){

    // ボタンが押されたらresolveする

    return new Promise(
            (resolve,reject) => {

                 btn.addEventListner( "click" ,
                             e => { resolve("click!!"); }
                   );

           });
}

 

これは次のような流れを期待しています。

 

  1. 関数aを実行
  2. 関数bを実行
  3. 関数cをawait付で実行
  4. ブラウザ上でボタンが押されるのを待つ
  5. 関数cが終了
  6. 関数bが終了
  7. 関数aが終了

 

実際には次のような流れになります。

 

  1. 関数aを実行
  2. 関数bを実行
  3. 関数cをawait付で実行
  4. 関数cがPromiseオブジェクトを作成して終了
  5. 関数aが終了。関数bは待機
  6. ブラウザ上でボタンが押されるのを待つ(関数cのPromise部分)
  7. 関数bのawait以降を実行し終了

 

つまりブラウザ上のボタンが押される前に関数aが終了します。

 

少しわかりにくいですが、図にしてみます。

 

async await 処理の流れ

 

asyncで定義した関数内なら、『awaitは処理を一時停止する』と言えます。
しかし大元の流れは一時停止しません。
このことに注意してプログラムの流れを制御する必要があります。

 

 

非同期部分の流れ

 

上のコードの、非同期部分の流れを図にしてみます。

 

async await 非同期処理の流れ

 

関数c内で登録したクリックイベントが発生しresolveでPromiseが解決されると、その値がawaitの戻り値としてセットされます。

 

戻り値を受け取った関数bはawaitキーワード以降を処理します。
上記の例ではコンソールに、『"click!!"』と表示されます。

 

最後に自身(関数b)の戻り値をPromiseのresolveに置き換えます。

 

さらに関数bの呼び出し元である関数aでは、then()メソッドで非同期処理の結果を受け取ります。
上記の例ではコンソールに、『"b end"』と表示されます。

まとめ

 

async/awaitは、Promiseオブジェクトの知識があることが前提のキーワードです。

 

しかしこれらのキーワードは、プログラムの大きな流れを変えてしまう非常に重要なものであり、知らないとコードを解析できません。

 

非常に初心者泣かせであり、教える側にとってもやっかいな機能です。

 

慣れれば問題ないのですが…

 

文句を言っていても仕方がないので、頑張って理解するしかないですね。

 

記事の内容について

 

こんにちはけーちゃんです。
説明するのって難しいですね。


「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。

そんなときは、ご意見もらえたら嬉しいです。

ご意見はこちら。
https://affi-sapo-sv.com/info.php

【お願い】

お願い

■このページのURL


■このページのタイトル


■リンクタグ