WebブラウザのJavaScriptはスレッドが一つしかありません。そのため、処理完了までに時間がかかる動作を行うと、他の動作ができない、いわゆるフリーズした状態になってしまいます。それはとてもUXが悪いです。
その状態を回避するために、非同期処理が生まれています。ネットワーク処理や、ファイル処理など様々な場面でJavaScriptは非同期処理を用いています。そして、mBaaSはあらゆる処理において非同期処理が行われています。非同期処理をマスターすることがmBaaSを使いこなすのに直結しているといっても過言ではありません。
この記事では非同期処理の一般的な手法であるPromise形式から、最近の書き方であるasync/awaitへの切り替え方を紹介します。
- PromiseによるmBaaS操作
- async/awaitへの書き直し
- 検索時
- async/awaitの注意点
- 既存の処理をasync/await化する
- ファイルの読み取りをasync/await化する
- まとめ
PromiseによるmBaaS操作
NCMBのドキュメントではすべての操作をPromise形式で記述しています。例えばデータストア (JavaScript) : 基本的な使い方 | ニフクラ mobile backendでは、データの保存法について次のように紹介しています。
var GameScore = ncmb.DataStore("GameScore"); var gameScore = new GameScore(); gameScore.set("score", 1337) .set("playerName", "Taro") .set("cheatMode", false) .save() .then(function(gameScore){ // 保存後の処理 }) .catch(function(err){ // エラー処理 });
async/awaitへの書き直し
上記のデータストア保存処理をasync/awaitに書き直すと、次のようになります。
try { var GameScore = ncmb.DataStore("GameScore"); var gameScore = new GameScore(); await gameScore.set("score", 1337) .set("playerName", "Taro") .set("cheatMode", false) .save(); } catch (err) { // エラー処理 }
gameScoreが保存された後のobjectIdはそのまま登録されていますので、返値を処理する必要はありません。エラー時は例外処理が発生するので、try/catchを使って補足しなければなりません。
検索時
データストアを検索して、その値からさらに何かデータを検索するといった処理はよくあるかと思います。Promiseで書くと次のようになります。Taroというユーザのスコアを取得し、それぞれのスコアに対して他に何人のユーザがいるのかを出すといった具合です。非同期処理の中で行う繰り返し処理は、ネストがどんどん深くなるのが難点です。
// プレイヤーがTaroのスコアを降順で取得 var GameScore = ncmb.DataStore("GameScore"); GameScore.equalTo("playerName", "Taro") .order("score",true) .fetchAll() .then(function(results){ console.log("Successfully retrieved " + results.length + " scores."); for (var i = 0; i < results.length; i++) { var object = results[i]; GameScore .equalTo("score", object.get("score")) .count() .fetchAll() .then(function(scores) { // 同じスコアのユーザが何人いるか分かる console.log(scores.count) }); } }) .catch(function(err){ console.log(err); });
同様の処理をasync/awaitで書いてみます。インデントがぐっと少なくなって、可読性がよくなっているのが分かるでしょうか。
try { var GameScore = ncmb.DataStore("GameScore"); var results = await GameScore.equalTo("playerName", "Taro") .order("score",true) .fetchAll(); console.log("Successfully retrieved " + results.length + " scores."); for (var i = 0; i < results.length; i++) { var object = results[i]; var scores = await await GameScore .equalTo("score", object.get("score")) .count() .fetchAll(); // 同じスコアのユーザが何人いるか分かる console.log(scores.count) } } catch (err) { console.log(err); }
async/awaitの注意点
async/awaitを使う場合の注意点としては、必ず関数で囲まれている必要があり、かつasyncと書かれていなければいけません。基本は次のような形です。
async function() { // この中ではawaitが使えます }
JavaScriptの場合 document.addEventListener
を使うと思いますので、その中で使う場合には次のようになります。どちらでもawaitが使えます。
// アロー関数の場合 document.addEventListener('DOMContentLoaded', async (e) => { }); // または document.addEventListener('DOMContentLoaded', async function(e) { });
JavaScriptでよく使うjQueryであっても同じです。
// アロー関数の場合 $(async () => { }); // または $(async function() { });
さらにイベント処理(クリックなど)でも同じように書けます。
$('#hoge').on('click', async (e) => { }); // または $('#hoge').on('click', async function(e) { });
既存の処理をasync/await化する
async/awaitは新しい書き方なので、これまでのJavaScriptライブラリと使い方がマッチしない場合もあります。よくあるのは、次のように引数を渡すケースです。
Hoge.exec(successHandler, errorHandler, options);
これをそのまま使うと、処理が長くなりがちです。オプションが一番最後だと、何の設定だったのか追うのも大変になります。
Hoge.exec(function(result) { // 成功時の処理 // : }, function(err) { // エラー時の処理 // : }, { // オプション a: 'b', c: 'd' })
そこで、次のような関数を用意します。関数名は適当です。この関数は、旧来の処理をPromise化しています。Promiseは2つの引数があり、最初が処理成功時、後ろが失敗時に呼び出されるものになります。
function Fuga(options) { return new Promise((res, rej) => { Hoge.exec(res, rej, options); } }
これを通常の処理で呼び出すには、次のようになります。
try { var result = await Fuga(options); // 成功時の処理 } catch (err) { // エラー時の処理 }
ファイルの読み取りをasync/await化する
次によくある例として、FileReaderを使ったファイル読み込みについて紹介します。これはユーザが指定した画像サイズを小さくしたり、Canvasに描画したりするのによく使われます。JavaScript でのローカル ファイルの読み込み - HTML5 Rocksのコードをサンプルにします。
function handleFileSelect(evt) { var files = evt.target.files; // FileList object // Loop through the FileList and render image files as thumbnails. for (var i = 0, f; f = files[i]; i++) { // Only process image files. if (!f.type.match('image.*')) { continue; } var reader = new FileReader(); // Closure to capture the file information. reader.onload = (function(theFile) { return function(e) { // Render thumbnail. var span = document.createElement('span'); span.innerHTML = ['<img class="thumb" src="', e.target.result, '" title="', escape(theFile.name), '"/>'].join(''); document.getElementById('list').insertBefore(span, null); }; })(f); // Read in the image file as a data URL. reader.readAsDataURL(f); } }
これをasync/awaitを使ってなるべくインデントが少ない状態にしてみます。元のコードが4つ分インデントしていたのに対して、async/awaitで書き直すと2つまで減っています。関数に分割することで、全体の見通しもよくなります。
async function handleFileSelect(evt) { var files = evt.target.files; // FileList object // Loop through the FileList and render image files as thumbnails. for (var i = 0, f; f = files[i]; i++) { await fileExecute(f); } } async function fileExecute(f) { return new Promise(async res => { // Only process image files. if (!f.type.match('image.*')) { return res(); } var e = await fileRead(f) var span = document.createElement('span'); span.innerHTML = ['<img class="thumb" src="', e.target.result, '" title="', escape(theFile.name), '"/>'].join(''); document.getElementById('list').insertBefore(span, null); res(); }); } async function fileRead(f) { return new Promise(res => { var reader = new FileReader(); reader.onload = res; reader.readAsDataURL(f); }); }
まとめ
ネットワークの非同期処理はPromiseで処理を行いますが、繰り返し使っていると徐々にネストが深くなって、コードの見通しが悪くなっていきます。async/awaitを使って、分かりやすい記述を心がけると、後々のメンテナンスも効率的になるでしょう。
ぜひasync/awaitでの記述にトライしてください。