世の中にあるJavaScriptのサンプルコードの多くは、昔からのブラウザをサポートしたJavaScriptコードになっています。
これには理由があり、デスクトップブラウザの中には新しいJavaScript構文に対応していないものが、まだ数多いためです。
しかし、モバイルについては違います。iOSやAndroidについてはブラウザが限定され、かつモダンなJavaScriptコードが利用できます。
そこで、今回はモダンなJavaScriptコードの筆頭とも言えるasync/awaitの書き方を紹介します。
これまでのコールバック方式やPromise方式を卒業して、見やすくてメンテナンスしやすいコードを書きましょう。
コールバック方式やPromise方式が利用される場所
Monacaアプリ開発ではコールバックやPromiseが多用されます。
これは非同期処理で使われるものになります。
非同期というのは、処理の完了を待たずに次の処理が実行され、後で処理完了が通知される仕組みです。
Monacaアプリでは例として、以下のような場所で非同期処理が用いられます。
- Cordovaプラグインの処理実行
- ネットワーク処理
- ファイル読み書き処理
これらの処理はアプリ開発の中でよく使われます。
非同期処理の書き方をマスターすれば一気にコードが見やすくなります。
Promiseからasync/awaitへの変更
Promiseとは then
や catch
を使った処理になります。ネットワーク処理で書いたことがある方も多いのではないでしょうか。
$.ajax(...)
.then(function(response) {
// 正常終了の場合
})
.catch(function(error) {
// エラーの場合
});
これだけだけであれば良いですが、複数回ネットワーク処理が発生する場合は、次のように徐々に複雑になります。
$.ajax(...)
.then(function(response1) {
// 正常終了の場合
// 別なネットワーク処理
return $.ajax(...);
})
.then(function(response2) {
// 別なネットワーク処理が正常終了した場合
})
.catch(function(error) {
// エラーの場合
});
上記の処理は以下のように書くこともできますが、ネスト(インデント)が深くなる分、コードの可読性は低くなります。
ネットワーク処理が2回、3回を続くとさらに大変になりそうなのことが分かってもらえるかと思います。
$.ajax(...)
.then(function(response1) {
// 正常終了の場合
// 別なネットワーク処理
$.ajax(...)
.then(function(response2) {
// 別なネットワーク処理が正常終了した場合
})
})
.catch(function(error) {
// エラーの場合
});
こうならないために利用するのがasync/awaitになります。
async/awaitの基本
async/awaitの基本は、functionの頭にasyncと書くことです。
asyncと書かれた関数の中でだけ、awaitが使えます。
async function() {
// この中でawaitが使えます
}
Monacaアプリの場合はボタンを押したり、タップしたりした際のイベントハンドリングを行うと思います。
その場合は次のように書けばOKです。
document.querySelector('#button').addEventListener('click', async function(e) {
// awaitが使える
});
なお、async/awaitが使えるのはasyncが書かれた関数内のみになります。
その関数の中で別な関数を作成している場合、そこではawaitが使えません。
そこでもasyncを記述しておく必要があります。これは画面の初期化時の後、イベント処理を記述する際によくあるミスです。
document.addEventListener('DOMContentLoaded', async function(e) {
// awaitが使える
document.querySelector('#button1').addEventListener('click', async function(e) {
// awaitが使える
});
document.querySelector('#button2').addEventListener('click', function(e) {
// awaitは使えない!
});
});
上記は素のJavaScriptですが、jQueryを使っていても同じように書けます。
$(async function() {
// awaitが使える
$('#button1').on('click', async function() {
// awaitが使える
});
$('#button2').on('click', function() {
// awaitは使えない!
});
});
Promiseからasync/awaitへの変更
Promiseからasync/awaitへの変更は簡単です。
非同期処理の前にawaitと書くと、thenで受け取っていた内容が実行結果として返ってきます。
つまり、以下2つの書き方で response は同じ内容になります。
// async/await版
const response = await $.ajax(...);
// Promise版
$.ajax(...)
.then(function(response) {
});
複数回の非同期処理が行われる場合にも、とてもシンプルに書けます。
Promiseで記述したものは、この記事の最初の方で取り上げたものになります。
// async/await版
const response1 = await $.ajax(...);
const response2 = await $.ajax(...);
Promiseで処理する場合、response1とresponse2が別な変数の空間にあるので、参照するのが難しかったですが、async/awaitであれば参照し合うのも簡単です。
catchはどう処理するのか
上記の例ではthenだけを対象としていますが、catchはtry/catch構文を使います。
async/awaitの場合、エラーが起きると例外処理になります。そこでtry/catchを使ってエラーを補足します。
以下のコードは同義です。
// async/await版
try {
const response1 = await $.ajax(...);
} catch (error) {
// エラーの場合
}
// Promise版
$.ajax(...)
.then(function(response) {
// 正常終了の場合
})
.catch(function(error) {
// エラーの場合
});
Cordovaプラグインをasync/await対応にする
ここから少し応用編です。Cordovaプラグインは通常、コールバック方式を利用しています。つまり、以下のような形です。
successCallbackが、処理成功時に呼ばれる関数。
failureCallbackが、処理失敗時に呼ばれる関数です。
window.cordova.plugin.PLUGIN_NAME
.exec(params, successCallback, failureCallback);
そのため、以下のように書くことが多いです。
しかし、これはとてもメンテナンスしづらいコードです。
以下の例はコメントだけなので良いですが、関数の内容が増えると処理を追いかけるのがとても大変になるでしょう。
window.cordova.plugin.PLUGIN_NAME
.exec(params,
function(response) {
// 処理成功時の処理
},
function() {
// 処理失敗時の処理
});
そこで、コールバック方式をPromiseの形に直してみます。
関数で囲むと簡単で、見やすくなります。基本形は以下のようになります。
new Promise
を使ってPromiseを定義します。
Promiseはその内部で処理成功時と失敗時、それぞれ呼び出す関数を受け取ります。
それをCordovaプラグインの成功時、失敗時の関数として指定するだけです。
// pluginFunctionという名前は適当です
function pluginFunction() {
return new Promise(function(response, reject) {
window.cordova.plugin.PLUGIN_NAME
.exec(params, response, reject);
})
}
使い方は、plugin_function
を await で呼び出すだけです。
const response = await pluginFunction();
async/awaitの弱点
async/awaitは非同期処理を順番に処理できるようになります。
つまり直列処理になります。
本来、非同期処理は処理の完了を待たずに次の処理に進めるのがメリットです。
しかし、awaitを使うことで処理が一時的に停止したように見えます。
await yourFunction();
// yourFunctionが終わるまで次に進まない = 進めない
これはメリットのように見えますが、デメリットになることもあります。
例えば写真をグリッド表示する際です。写真が数多くあり、一気にダウンロードしたい場合でも、awaitを使うと1枚ずつ写真をダウンロードする形になります。
これでは遅くて使いづらいでしょう。しかし、写真を全部ダウンロードするまで、次の処理にいっては困る場合もあります。
例えば次のように書くと、写真が1枚ずつダウンロードされていくのでUXが悪くなります。
for (const fileName of files) {
// fileDownload関数は非同期処理
await fileDownload(fileName);
}
// 写真ダウンロードが終わったら次の処理へ…
こうした時にはPromise.allを使って複数の非同期処理完了を待つことができます。
const promises = [];
for (const fileName of files) {
// fileDownload関数は非同期処理
promises.push(fileDownload(fileName));
}
// 全部のfileDownload関数実行完了を待つ
const photos = await Promise.all(promises);
// 写真ダウンロードが終わったら次の処理へ…
ループ処理でawaitを使うのは良くない処理なので、Promise.allを使っていくようにしましょう。
まとめ
今回の例のようにPromiseやコールバック方式はasync/awaitに置き換えていきましょう。使い方を覚えてしまえば、JavaScriptのコードが一気に見通し良くなるはずです。
メンテナンスしやすいコードは不具合を含みづらく、修正も容易になります。皆さんのアプリを継続的に更新していくためにも、ぜひ使いこなしてください。