【JavaScript】 async/awaitを解説します
更新日:2024/02/27
JavaScriptのasyncとawaitというキーワードは、非同期処理を簡潔に記述することができます。
しかしプログラムの流れを変えてしまう面もあり、理解するのが難しい機能でもあります。
そこで今回は、asyncとawaitについて解説します。
async/awaitの概要
asyncとawaitを端的に表すと、次のようになります。
■asyncとは
asyncは関数やメソッドで使用するキーワードです。
asyncを使用して定義された関数は、実行するとPromiseを生成して返します。
■awaitとは
awaitはPromiseオブジェクトに使用するキーワードで、async関数内のみで使用できます。
awaitは非同期処理の結果がでるまでコードを停止します。
結果が出たら、再開して後に続くコードを実行します。
ただしこれはコードの流れ上の話で、停止中はタイマーなどの他の非同期処理も処理されます。
async/awaitはPromiseの知識が必須となります。
Promiseについては、次のページを読んでみてください。
■【JavaScript】 非同期はPromise?解説が難しいので自分で理解してみた
asyncとは
関数定義および関数式の前にasyncキーワードを付加すると、AsyncFunctionオブジェクトを生成します。
AsyncFunctionオブジェクトは関数として実行可能で、実行するとPromiseを返します。
AsyncFunctionオブジェクトは、async関数や非同期関数とも呼ばれます。
※この記事ではasync関数と呼びます。
async関数を実行するとPromiseを返す
async関数は、実行するとPromiseを返します。
次のようにasync関数を実行して戻り値を確認すると、Promiseオブジェクトであることがわかります。
const asyncFuncObj = async () => {
return 1;
};
const result = asyncFuncObj();
console.log( result ); // Promise { <state>: "fulfilled", <value>: 1 }
そしてasync関数でreturnした値が、thenで通知されます。
result.then( e=>console.log(e) ); // 1
// then( console.log ) に置き換え可能
この動作から例で挙げたasyncFuncObjは、次のPromiseを返す関数に置き換えることができます。
const promiseFuncObj = () => {
return new Promise( resolve => {
resolve(1);
});
};
const result = promiseFuncObj();
console.log( result ); // Promise { : "fulfilled", : 1 }
result.then( e=>console.log(e) ); // 1
ただし上記の関数内では次項で解説する awaitを使用できません。
FunctionオブジェクトとAsyncFunctionオブジェクト
次のコードは、関数式がFunctionオブジェクトのインスタンスを生成していることを表しています。
const funcObj = function(){};
console.log( Object.getPrototypeOf(funcObj) === Function.prototype ); // true
Functionコンストラクターから生成されたオブジェクトは、プロトタイプチェーンの参照先がFunction.prototypeになります。
上のコードの2行目は、そのことを確認しています。
asyncを使用すると、Function.prototypeはプロトタイプチェーンの2階層目にセットされます。
const asyncFuncObj = async () =>{};
// 1階層目を取得
const asyncFuncObjProto = Object.getPrototypeOf(asyncFuncObj);
console.log( asyncFuncObjProto === Function.prototype ); // false
// 2階層目を取得
console.log( Object.getPrototypeOf(asyncFuncObjProto) === Function.prototype );
上のコードから、次のような階層になっているのがわかります。
asyncFuncObj :{ [[Prototype]] : { ← AsyncFunction.prototype [[Prototype]] : { ← Function.prototype } } }
※[[Prototype]]はプロトタイプチェーン
JavaScriptの言語仕様であるECMAScriptでは、asyncで生成されたオブジェクトはAsyncFunctionのインスタンスと定義されています。
しかしAsyncFunctionオブジェクトはグローバル定義されていないため、コード上で比較できません。
つまりasync関数がAsyncFunctionのインスタンスであることを証明する方法がないのですが、constructorオブジェクトのnameプロパティを見ると"AsyncFunction"であることを確認できます。
console.log( asyncFuncObjProto.constructor.name ); // "AsyncFunction"
awaitとは
awaitキーワードは、Promiseオブジェクト前に付加することで、非同期処理の結果が出るのを待ちます。
ただし、いくつかの条件があります。
awaitキーワードはasyncキーワード内のみ使用可能
awaitはasyncキーワードが付加された関数内でのみ使用可能です。
awaitはasync内でのみ使用可能
const b = async ()=>{
const x = 2;
const r = await c();
return x * r;
}
awaitは複数指定できます。
awaitを複数使用可能
const b = async ()=>{
const x = 2;
const r = await c();
const r2 = await d();
return x * r * r2;
}
asyncはイメージ的には、return new Promiseの糖衣構文ですが、そちらでは使用できません。
awaitはasync内でのみ使用可能
const b = ()=>{
return new Promise( (resolve, reject) =>{
const x = 2;
const r = await c(); // エラー!!
resolve ( x * r );
});
}
awaitキーワードはPromiseオブジェクトのみ使用可能
awaitキーワードはPromiseオブジェクトの前のみ記述可能です。
Promiseオブジェクトにawait付加
async function b(){
const a = new Promise( (resolve, reject) =>{
document.getElementById("btn").addEventListener("click", ()=>{
resolve( true );
});
});
return await a;
}
実際には、Promiseオブジェクトを返す関数に付加することが多いです。
Promiseオブジェクトを返す関数にawait付加
function clickWait(){
return new Promise( (resolve, reject) =>{
document.getElementById("btn").addEventListener("click", ()=>{
resolve( true );
});
});
}
async function b(){
return await clickWait();
}
async関数もPromiseオブジェクトを返すので、awaitを使用できます。
async関数にawait付加
function clickWait(){
return new Promise( (resolve, reject) =>{
document.getElementById("btn").addEventListener("click", ()=>{
resolve( true );
});
});
}
async function a(){
return await clickWait();
}
async function b(){
return await a();
}
awaitの結果は戻り値で取得する
awaitが付加されたPromiseでresolve()やreject()された結果は、then()やcatch()などで登録したコールバックで処理された後、最終的な値を戻り値として受け取ることができます。
awaitの結果を取得
async function b(){
return 3; // resolve(3)と同値
}
async function c(){
const x = 2;
const r = await b(); // 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;
}
上のコードは、catch()で返された値が変数rにセットされています。
ここでは戻り値を指定していないので、値はundefinedです。
次のように、値を返すようにしておきましょう。
catchの戻り値を受け取る
function b(){
return new Promise( (resolve, reject) =>{
reject("error!!");
});
}
async function c(){
const x = 2;
const r = await b().catch( e =>null );
if( r === null ) return 0; // エラー判定
const y = 4;
return r * x * y;
}
ただし自作関数では、できれば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キーワードに到達するとそれ以降はPromiseのコールバックとしてスケジューリングされます。
次のようなコードで考えてみます。
const b = async ()=>{
const x = 10; // ①
const r = await c(); // ②
const r2 = await d(); // ③
return x * r * r2; // ④
}
関数bを実行すると①が処理されます。
次に②の関数cを実行するとPromiseが返されます。
そしてPromiseの最終的なコールバックとして、変数rへの代入と③④の実行が登録され関数bの処理が終了します。
関数bを呼び出した一連のコードが終了すると、イベントループでタスクが処理されます。
そのため、async関数のコード上は停止しているように記述されていますが、実際にはタイマーやマウスクリックなどのイベントが発生します。
Promiseの結果が出るとコールバックが呼ばれ、変数rへの代入が行われます。
そして③のawaitで同様に残りの処理がコールバック登録され、処理が終了します。
再度Promiseの結果を待って、残りの処理が実行されます。
async/awaitの使用例
async/awaitを使用した、簡単な例を挙げてみます。
実行例
『アンケートに答える』を押してアンケートにご協力ください。
あなたの性別は?
html
『アンケートに答える』を押してアンケートにご協力ください。
<button id="openBtn" >アンケートに答える</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="okBtn">決定</button><button id="canselBtn">キャンセル</button></p>
</div>
JavaScript
window.addEventListener( "DOMContentLoaded" , ()=> {
/**
* アンケート答えるボタンクリック処理
*/
(()=>{
const [openBtn,div] = ["openBtn","div1"].map( id=>document.getElementById(id) );
openBtn.addEventListener("click",()=>{
[openBtn.disabled,div.style.display] = [true,"block"];
questionnaire().then(e=>{
alert(e);
[openBtn.disabled,div.style.display] = [false,"none"];
});
});
})();
/**
* アンケート受付処理
* @returns {Promise}
*/
const questionnaire = async () => {
const res = await decision();
return res === null ? "キャンセルしました" : `あなたは${res}です`;
};
/**
* 性別取得
* @returns {Promise}
*/
const getSeibetu = (()=>{
const seibetuSelect = document.querySelectorAll("input[name='seibetu']");
return ()=>seibetuSelect[ seibetuSelect[0].checked ? 0 : 1].value;
})();
/**
* 決定・キャンセルボタン処理
* @returns {Function}
*/
const btnOn = (()=>{
let resolve = null;
[["okBtn","ok"],["canselBtn","cansel"]].forEach(
([id,result]) =>
document.getElementById( id )
.addEventListener("click",()=>{
if( typeof resolve === "function" ) {
resolve( result );
resolve = null;
}
})
);
return r => resolve = r;
})();
/**
* 決定・キャンセルボタンクリック待ち
* @returns {Promise}
*/
const decision = () =>
new Promise( resolve =>btnOn( resolve ) )
.then( e=> e === "cansel" ? null : getSeibetu() );
});
アンケートといっても、ここでおこなっていることは次の二つだけです。
(1) 『アンケートに答える』ボタンが押されたら、アンケートを表示してアンケートの結果が出るまでボタンを無効にする
(2) 決定またはキャンセルボタンが押されたら結果をアラート表示して、アンケートを消し、アンケートの結果が出るまでボタンを有効にする
ここでのポイントは、アンケートの処理をasync関数内に閉じ込め、『アンケートに答える』ボタンのイベント内では、ボタンの有効無効およびアンケート画面の表示非表示の切り替えだけをおこなっている点です。
まとめ
async/awaitは、プログラムコードの構成をJavaScriptのシステムが内部で変更しています。
そのため、思っていたのと異なる結果となったと感じる人も多いと思います。
Promiseオブジェクトの知識があることが前提のキーワードです。
Promiseがわかり難いのに、さらにasync/awaitがわかり難くしてくれます。
非常に初心者泣かせであり、教える側にとってもやっかいな機能です。
慣れれば問題ないのですが…
文句を言っていても仕方がないので、頑張って理解するしかないですね。
更新日:2024/02/27
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。