MENU

JavaScript同期・非同期構文

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

更新日:2020/07/14

 

 

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

 

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

 

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

 

asyncとawaitの処理の流れ

 

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

 

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

 

なおasyncとawaitは次の例の形式をそのまま当てはめれば利用可能ですが、Promiseオブジェクトの知識があると理解が深まります。
Promiseオブジェクトについては次の記事で紹介しているので、確認してみてください。
参考記事:【JavaScript】 非同期はPromise?解説が難しいので自分で理解してみた

 

メインの流れ

 

async await 処理の流れ

 

上の図は、関数aからasyncキーワードを付与した関数bを呼び出し、関数b内で関数cをawaitキーワードを使って呼び出しています。
関数cは、ボタン要素がクリックされたらresolveで解決するPromiseを返します。

 

このとき関数b内で関数cを実行後、それ以降の処理をおこなわずに呼び出し元の関数aに戻ります

 

上述の『awaitは処理を一時停止する』または『待機する』という表現は、関数b内で関数cが解決されるまで一時停止して、解決されたら関数c以降の処理を実行。
そして関数bが終了したら、関数aに戻る。

 

このような意味にとってしまいます。
現に僕もこの意味にとってしまい、思ったような動きをしてくれずに悩みました。

 

『awaitは処理を一時保留して呼び出し元に戻る』という表現が適切です。

 

非同期部分の流れ

 

次に、メインの流れで処理されなかった非同期部分の流れを図にしてみます。

 

async await 非同期処理の流れ

 

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

 

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

 

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

 

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

asyncとは

 

asyncは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オブジェクトを返す関数のみ使用可能です。

 

これまで紹介したreturn new Promise()をおこなっている関数だけでなく、async関数も指定できます。

 

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は、Promiseオブジェクトの知識があることが前提のキーワードです。

 

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

 

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

 

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

 

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

 

記事の内容について

 

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


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

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

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

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