JavaScriptの初級者と中級者を分ける大きな境目は非同期処理の書き方にあると思います。WebブラウザでJavaScriptを書いている時、ネットワーク処理などで非同期処理を使うことは多々あります。複雑な書き方をしてしまうとスパゲティコードになってしまい、見通しが悪くなったり、メンテナンスしづらいコードになってしまうでしょう。
そこで今回は非同期処理を分かりやすく書く方法について紹介します。
基本的な書き方
JavaScriptの非同期処理はPromiseというオブジェクトを使って行います。Promiseは処理がうまくいった場合にはthen、失敗した場合にはcatchというメソッドに処理を返します。
例えばニフクラ mobile backendのデータストア処理もPromiseを使っています。そこでデータを取得して表示する部分は次のように書けます。
const Item = ncmb.DataStore('Item') Item .fetchAll() .then(function(results) { // データを処理する }) .catch(function(error) { // エラー処理 });
もし、このresultsを使って、さらにデータを取り出す場合には、チェーンメソッドが使えます。
const Item = ncmb.DataStore('Item') Item .fetchAll() .then(function(results) { // データを処理する const Item2 = ncmb.DataStore('Item2'); return Item2 .equalTo('name', results[0].name) .fetchAll(); }) .then(function(results2) { // Item2の取得結果 }) .catch(function(error) { // エラー処理 });
このように書いた場合、 // Item2の取得結果
の中において、results の内容に触れることはできません。変数が定義されていないというエラーになってしまうでしょう。そこで、次のように書かざるをえません。
let res1 // どこでも使える変数を追加する const Item = ncmb.DataStore('Item') Item .fetchAll() .then(function(results) { // データを処理する res1 = results; // 結果をres1に入れる const Item2 = ncmb.DataStore('Item2'); return Item2 .equalTo('name', results[0].name) .fetchAll(); }) .then(function(results2) { // Item2の取得結果 // res1が使える }) .catch(function(error) { // エラー処理 });
これがさらに非同期処理が重なっていくと、変数の管理が煩雑になっていきます。これはとても面倒です。
async/awaitを使う
そこで使いたいのがasync/awaitです。async/awaitでは、thenで受け取る内容だった結果を受け取れるようになります。つまり、上記の処理であれば次のように書けます。
const Item = ncmb.DataStore('Item') const results = await Item.fetchAll(); const Item2 = ncmb.DataStore('Item2'); const results2 = await Item2 .equalTo('name', results[0].name) .fetchAll();
ただしawaitはasyncの中でしか使えません。従って、次のように関数で囲む必要があります。
async function main() { const Item = ncmb.DataStore('Item') const results = await Item.fetchAll(); const Item2 = ncmb.DataStore('Item2'); const results2 = await Item2 .equalTo('name', results[0].name) .fetchAll(); } main();
関数を定義して実行するのが面倒な場合、匿名関数を使って書くこともできます。
(async function() { const Item = ncmb.DataStore('Item') const results = await Item.fetchAll(); const Item2 = ncmb.DataStore('Item2'); const results2 = await Item2 .equalTo('name', results[0].name) .fetchAll(); })();
さらに function と書かずにアロー関数にすることもできます。
(async () => { const Item = ncmb.DataStore('Item') const results = await Item.fetchAll(); const Item2 = ncmb.DataStore('Item2'); const results2 = await Item2 .equalTo('name', results[0].name) .fetchAll(); })();
Webアプリケーションの場合、DOMContentLoadedを待ってから処理を書くと思いますので、次のように書くのがお勧めです。
document.addEventListener('DOMContentLoaded', async () => { // awaitが使える環境です });
エラー処理について
async/awaitで、catch相当の処理を使うには try/catchを使います。
(async () => { try { const Item = ncmb.DataStore('Item') const results = await Item.fetchAll(); const Item2 = ncmb.DataStore('Item2'); const results2 = await Item2 .equalTo('name', results[0].name) .fetchAll(); } catch (error) { // エラー処理 } })();
なお、この書き方の場合、 Item.fetchAll
でエラーなのか、 Item2.fetchAll
でエラーなのか分かりづらいという問題もあります。さらにいえば、results2がどこかで定義されている場合に起こるJavaScriptのエラーでもcatchへ飛んでしまうという問題もあります。
そこで、次のような関数を使うことで、エラー制御を簡単にするアイディアもあります。解説はしませんので、どういった意味があるのか考えてみてください。
const p = (func) => { return new Promise(res => { func .then(result => res(result, null)) .catch(error => res(null, error)); }) }
この p という関数は、2つの結果を返します。1つ目は処理が正しく終了した時の変数、2つ目はエラーの時の内容です。つまり、この関数を使うと、async/awaitが次のように書けます。
(async () => { const Item = ncmb.DataStore('Item') let [results, error] = await p(Item.fetchAll()); if (error) { // エラー処理 } const Item2 = ncmb.DataStore('Item2'); let [results2, error] = await p(Item2 .equalTo('name', results[0].name) .fetchAll()); if (error) { // エラー処理 } })();
こうするとインデントが減るので、コードの見通しがよくなります。
まとめ
なお、Promiseのいいところとしては処理を並列で実行してくれる点が挙げられます。async/awaitを使った場合、処理は直列になるので、awaitの結果が返ってくるまで次の処理はできなくなります。Promiseの場合は、結果を待たずに実行してくれるので、複数のネットワーク処理をまとめて行うのには便利です。
分かりやすいコードを書くのは、中長期的なメンテナンスを行う上で大事な要素です。コードがスパゲティになってメンテナンスしづらくなると感じる方はぜひ注意して取り組んでみてください。