JavaScript 再入門(その23) 非同期処理 4 非同期関数 async / wait

非同期関数 async / wait

前回までの記事でプロミスの動きを見てみました。

JavaScript 再入門(その20) 非同期処理 1 非同期処理とPromise
非同期処理とPromise 非同期処理とは? まずは、通常の処理方法、同期処理から説明します。通常のプログラムは上から順番にコードを処理して、処理が終わると次の…
sakura-system.com
JavaScript 再入門(その21) 非同期処理 2 プロミスの動作
プロミスの動作 前回の記事でプロミスの大まかな動きを見てみました。 プロミスって何をしてるの? 今回は、プロミスの状態や値の受け渡しなどの、プロミスの動作につい…
sakura-system.com

非同期関数

いままで、ややこしいプロミスのスクリプトを書いてきましたが。
ES2017 で、非同期処理をまるで同期処理のようにサラッと書ける救世主が現れました。
それが、async / await *ES2017 です。

async キーワードは関数につけて、その関数が非同期処理をおこなう非同期関数であることを宣言します。
await キーワードは、必ず async キーワードで宣言された非同期関数の中でのみ宣言できます。

await キーワードで宣言された式は非同期処理が解決するまで、次の処理にうつりません
プロミスベースの非同期の動作を、プロミスチェーンを明示的に構成する必要なく、よりすっきりとした方法で書くことができます。

非同期関数は常にプロミスを返します。
返り値が明示的にプロミスでないときは、プロミスでラッピングされます

async function asyncF() {
	return 'This is Async function !' ;
}
console.log( asyncF() ) ;
/*
Promise {<fulfilled>: 'This is Async function !'}
	[[Prototype]]: Promise
	[[PromiseState]]: "fulfilled"
	[[PromiseResult]]: "This is Async function !"
*/

非同期関数を then() でつないでプロミスチェーンで利用することもできます。

async function asyncF() {
	return 'This is Async function !' ;
}
asyncF().then( ( ret ) => {
	console.log( ret ) ;
	return asyncF() ;
}).then( ( ret ) => {
	console.log( ret ) ;
	return asyncF() ;
}).then( ( ret ) => {
	console.log( ret ) ;
}) ;
/*
This is Async function !
This is Async function !
This is Async function !
*/

プロミスチェーンからも開放

前の記事で作成した、非同期処理でプロフィールを作成するスクリプトを、非同期関数を使って書き直します。

let profile = '' ;	// プロフィール
let dataSet = {		// プロフィールデータ
	carName : 'AE86 SPRINTER TRUENO GT-APEX 3door ' , // 車名
	firstName : 'Takumi ' ,		// 名前
	lastName : 'Fujiwara ' ,	// 苗字
}
// 非同期でリクエストに対するデータをX秒後に返す関数
let GetReqData = function( argReq ) {
	return new Promise( ( resolve , reject ) => {	// 非同期処理の結果を Promise で返します
		setTimeout(
			() => {
				if ( argReq in dataSet ) {			// リクエストがプロフィールデータに存在するか
					resolve( dataSet[ argReq ] ) ;	// リクエストが正しければ正常終了して値も返すよ! 
				} else {
					reject( argReq ) ;				// リクエストが正しくなければリクエストをエラーとして投げる
				}
			} , 
			Math.random() * 1000
		) ;
	}) ;
}
//
async function getProfile() {	// async 非同期関数として宣言 ***
	let car = new GetReqData( 'carName' ) ;		// 車名取得インスタンス生成
	let lname = new GetReqData( 'lastName' ) ;	// 苗字取得インスタンス生成
	let fname = new GetReqData( 'firstName' ) ;	// 名前取得インスタンス生成
	// 非同期で 車名+苗字+名前 のプロフィールを取得したい
	try {
		profile += await car ;		// 非同期処理が終わるまで次の処理にうつらない ***
		profile += await lname ;	// 非同期処理が終わるまで次の処理にうつらない ***
		profile += await fname ;	// 非同期処理が終わるまで次の処理にうつらない ***
	} catch( e ) {
		console.error( 'Request ' + e + ' is not found !' ) ;
	}
	console.log( 'Profile:' + profile ) ;	// プロフィールを表示
}

getProfile() ;				// 非同期関数でプロファイルを取得して表示する。
console.log( '<TOYOTA>') ;	// 一番最初に表示される。
/*
<TOYOTA>
Profile:AE86 SPRINTER TRUENO GT-APEX 3door Fujiwara Takumi
*/

コールバック地獄からだけでなくプロミスチェーンからも開放されてしまいました。

プロミスチェーンを使った処理

参考までにプロミスチェーンを使った同じ処理のスクリプトです。

let profile = '' ;	// プロフィール
let dataSet = {		// プロフィールデータ
	carName : 'AE86 SPRINTER TRUENO GT-APEX 3door ' , // 車名
	firstName : 'Takumi ' ,		// 名前
	lastName : 'Fujiwara ' ,	// 苗字
}
// 非同期でリクエストに対するデータをX秒後に返す関数
let GetReqData = function( argReq ) {
	return new Promise( ( resolve , reject ) => {	// 非同期処理の結果を Promise で返します
		setTimeout(
			() => {
				if ( argReq in dataSet ) {			// リクエストがプロフィールデータに存在するか
					resolve( dataSet[ argReq ] ) ;	// リクエストが正しければ正常終了して値も返すよ! 
				} else {
					reject( argReq ) ;				// リクエストが正しくなければリクエストをエラーとして投げる
				}
			} , 
			Math.random() * 1000
		) ;
	}) ;
}

// コールバック関数(プロフィールの編集)
let editProfile = ( argProfData ) => {
	profile += argProfData ;				// プロフィールに返された値を付加する。
}
//
let car = new GetReqData( 'carName' ) ;		// 車名取得インスタンス生成
let lname = new GetReqData( 'lastName' ) ;	// 苗字取得インスタンス生成
let fname = new GetReqData( 'firstName' ) ;	// 名前取得インスタンス生成
// 非同期で 車名+苗字+名前 のプロフィールを取得したい
car.then( ( argRes ) => {				// 車名の取得と車名の取得が成功(resolve)したときの処理
	editProfile( argRes ) ;				// 車名をプロフィールに編集
	return lname ;							// 苗字の取得 return によって結果は次の then() 関数の引数になる
}).then( ( argRes ) => {				// 苗字の取得が成功(resolve)したときの処理
	editProfile( argRes ) ;				// 苗字をプロフィールに編集
	return fname ;							// 名前の取得 return によって結果は次の then() 関数の引数になる
}).then( ( argRes ) => {				// 名前の取得が成功(resolve)したときの処理
	editProfile( argRes ) ;				// 名前をプロフィールに編集
}).catch( ( e ) => {						// 失敗時の処理
	console.error( 'Request ' + e + ' is not found !' ) ;
}).finally( () => {							// 終了処理
	console.log( 'Profile:' + profile ) ;	// プロフィールを表示
}) ;
console.log( '<TOYOTA>') ; // 一番最初に表示される。
/*
<TOYOTA>
Profile:AE86 SPRINTER TRUENO GT-APEX 3door Fujiwara Takumi
*/

コールバックを使った処理

そして、コールバックを使ったスクリプトです。

let profile = '' ; // プロフィール
let dataSet = { // プロフィールデータ
	carName : 'AE86 SPRINTER TRUENO GT-APEX 3door ' , // 車名
	firstName : 'Takumi ' , // 名前
	lastName : 'Fujiwara ' , // 苗字
}
// 非同期でリクエストに対するデータをX秒後に返す関数 エラー処理あり
let getReqData = function( argReq , argCallback , argFailCallback ) {
	setTimeout(
		() => {
			if ( argReq in dataSet ) { // リクエストがプロフィールデータに存在するか
				argCallback( dataSet[ argReq ] ) ; // 正常終了
			} else {
				argFailCallback( argReq ) ; // リクエストが不正なためエラー
			}
		}  ,  // データをコールバックに渡す
		Math.random() * 1000 // 延滞時間はランダムに設定
	) ;
}
// コールバック関数(プロフィールの編集)
let editProfile = ( argProfData ) => {
	profile += argProfData ;
	console.log( profile ) ;
}
// エラー時のコールバック関数
let errorProfile = ( argFailReq ) => {
	throw 'Request ' + argFailReq + ' is not found!' ;
}
// 非同期で 車名+苗字+名前 のプロフィールを取得する
try {
	getReqData( 'carName' , ( retArg ) => { // 車名取得
		editProfile( retArg ) ;
		getReqData( 'lastName' , ( retArg ) => { // 苗字取得
			editProfile( retArg ) ;
			getReqData( 'firstName' , ( retArg ) => { // 名前取得
				editProfile( retArg ) ;
			} , errorProfile );
		} , errorProfile );
	} , errorProfile );
} catch (e) {
	console.error(e) ;
}
console.log( '<TOYOTA>') ; // 一番最初に表示される。
/*
<TOYOTA>
Profile:AE86 SPRINTER TRUENO GT-APEX 3door Fujiwara Takumi
*/

参考リンク

MDN 開発者向けのウェブ技術 > 非同期関数
MDN 開発者向けのウェブ技術 > 標準組み込みオブジェクト > Promise
MDN 開発者向けのウェブ技術 > プロミスの使用
MDN 開発者向けのウェブ技術 > JavaScript「再」入門