筆者は、普段はC#やC++など、型付きの言語を使ってデスクトップアプリケーションを作ることが多いです。そんな筆者が、最近、Webのクライアントサイドのプログラム(主にJavaScript)を書き始めたのですが、いろいろと苦労することが多いです。そんな中、TypeScriptを使うことで、Webクライアントサイドのプログラムも型付きの言語で行えるようになりました。
とはいえ、それ以外にも苦労する部分は多いです。そのうちの一つが、非同期処理のオンパレード。しかも、本当のスレッドではないため、イベント待ちなどもできず、コールバック関数をうまく使って書いてあげなければいけません。これがなか慣れませんし、記述も複雑です。その問題に対し、TypeScriptにはasync/awaitという非同期処理を同期的(?)に掛ける構文があります。この構文、以前はES6(ECMAScript2015、つまりそのまま実行できるブラウザは少ない)へのトランスパイルしか対応していなかったのですが、最近ではES5/ES3へもトランスパイルできるようになったため、グッと使いやすくなりました。
TypeScriptのasync/awaitの使い方に関しては、多くの記事がありますが、いざ使ってみようと思ったときに躓くことがいくつかありました。そのため、この記事では、それらについてまとめて行きたいと思います。
#async/await構文の使い方
非同期処理が終わるまで待ってくれる構文です。例えば、XMLHttpRequestを使ってJSONファイルを読み込むような処理を、以下のように書くことができます。(「=>」は、関数を定義するための構文です。)
function loadData(url: string) {
return new Promise(resolve => {
let xhr = new XMLHttpRequest();
xhr.onload = ()=>{
resolve(JSON.parse(xhr.response));
};
xhr.open("GET", url);
xhr.send(null);
});
}
async function start() {
let obj = await loadData("./data.json");
console.log(obj);
}
start();
start関数は、data.jsonというJSONファイルを読み込み、結果をコンソールに出力する関数です。loadData関数は、urlを受け取り、データを読み込んで、JSONをパースして返してくれる関数です。説明の都合上、start関数を非同期処理呼び出し関数、loadData関数を非同期関数と呼びます。さて、使い方のポイントは、以下の5つです。
- 非同期処理呼び出し関数には、asyncをつける
- 非同期関数は、Promiseオブジェクトを返す
- 非同期処理が終わったら、resolveを呼ぶ
- 非同期関数の戻り値は、resolveに渡す
- 非同期関数を呼び出す際には、awaitをつける
以上のように書くと、start関数内では、loadData関数の処理が終わる(正確には、resolveが呼び出される)まで待ってから、返り値を受け取り、次の処理に進むようになります。それなりに書くことが多いのですが、それでも、async/awaitを使わないよりは、随分と楽になります。以下に、async/awaitを使わない場合を書いてみます。
function loadData(url: string, callBack) {
var xhr = new XMLHttpRequest();
xhr.onload = callBack;
xhr.open("GET", url);
xhr.send(null);
}
function start() {
loadData(
"./data.json",
function(e){
console.log(JSON.parse(this.response));
}
);
}
デスクトップアプリケーションを書いている人からすると、async/awaitを使った方が随分書きやすいと思います。この記事では省略しますが、エラー処理などもシンプルに書くことができます。
#で、Promiseってなんなのさ?
使えるととても便利なasync/await構文なのですが、実体は先ほどさらっと登場したPromiseのシンタックスシュガー(要は、短く書ける構文)です。async/awaitの紹介記事では
//非同期関数
function asyncFunc()
{
// 処理
}
async function foo()
{
await asyncFunc();
}
のように紹介されていることがありますが、JavaScriptには詳しくない筆者からすると、asyncFunc()の書き方がわからずに、ハマりました。そのため、この記事では非同期関数の書き方についても簡単に説明したいと思います。(Promiseについてはこちらをご覧ください。)
先に書いた通り、awaitで待つ関数は、必ずPromiseオブジェクトを生成して返すようにします。Promiseを生成する際には、最初に処理する関数を渡します。つまり、非同期関数は以下のようなテンプレートになると思います。
function asyncFunc()
{
return new Promise( 処理する関数 );
}
Promise生成時に渡す関数は、少なくとも処理が完了した際に呼び出す関数を第一引数に受け取るような関数を渡す必要があります。少なくともと書いた理由は、実際には、第一引数には処理が成功した際に呼び出す関数が渡され、第二引数には失敗した際に呼び出す関数が渡されるためです。さて、それを踏まえると、テンプレートとしては以下のようにしておくと良さそうです。
function asyncFunc()
{
return new Promise( function(resolve, reject){
//実際の処理
});
}
TypeScriptのアロー構文を使うならこんな感じ。
function asyncFunc()
{
return new Promise( (resolve, reject) => {
//実際の処理 (終わったら、resolveを呼ぶ)
});
}
あとは、処理の完了後に第一引数として渡されるresolve関数を呼び出せばOKです。awaitで呼び出された際に返す返り値は、resolveの引数に渡します。Promiseを使っても、結局、関数が何重かネストしてしまいますが、ほぼこのテンプレートで問題ないと思います。
Promiseって、使って大丈夫なの?
Promiseは、ECMAScript2015(ES6)から導入された構文です。ES6?ブラウザで対応してる?と思われるかもしれませんが、現在のモダンブラウザでは問題なく対応しています。ただ、IE11は非対応なため、それに対してはこちらの記事をご覧ください。
Visual Studio Codeに怒られるんだけど…?
さて、ES5を対象にした状態で、Promiseオブジェクトを使うと、Visual Studio Codeに赤い波線を引かれて怒られることがあります。これは、ES5ではPromiseの定義がないためです。そこで、tsconfig.jsonにPromiseのライブラリを追加します。以下のような感じです。
{
"compilerOptions": {
"target": "ES5",
"module": "amd",
"sourceMap": true,
"outFile": "./src/main.js",
//libを追加
"lib": [
"dom",
"es5",
"scripthost",
"es2015.promise" //←ポイント
]
}
}
libに「es2015.promise」を追加することで、警告が消えます。そのほかはデフォルトのものなのですが、追加する構文がわからなかったため、デフォルトのものもすべて書いています。
#まとめ
TypeScriptで便利なasync/awaitについて紹介しました。筆者はJavaScriptに不慣れなため便利そう!と飛びついたのですが、Promiseの使い方や、それに絡む実際の運用上の問題、Visual Studio Codeの設定につまずいたため、その点を重点的に解説しました。なお、筆者が以前書いた開発環境の構築記事では、Promiseの問題に対応しています。
外部リソースを読み込もうと思うと、XMLHttpRequestを使ってリソースを読み込んだりするのですが、その完了を知るためには、コールバック関数を登録して、その呼び出しを待たないといけません。もし、XMLHttpRequestが本当のマルチスレッドなら、読み込みを呼び出して、呼び出した関数内からイベント待ちなどをすればよいのですが、残念ながらJavaScriptはシングルスレッドのため、XMLHttpRequestが仕事をできるようにするためには、一旦呼び出した側の関数から抜け出て、読み込み完了後の処理はXMLHttpRequestに登録したコールバック関数内で行わなくてはいけません。それに対し、async/awaitを使うと、随分と楽に書けるようになります。今回の例ではそれほど実感はないと思いますが、例えば5ファイルからデータを読みながら逐次的に処理をしなければいけない場合などには特に重宝すると思います。
利用例などを追加したものを執筆しましたので、こちらも御覧ください。
TypeScriptのasync/awaitとPromise
#参考