JavaScriptやAjaxでの処理順を正確に制御する7つの方法

JavaScriptの処理順を制御するモダンなコーディングJS
この記事は約23分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●JavaScriptの処理順と実行タイミング

JavaScriptを書いていると、思った通りの順番で処理が実行されないことがありますよね。

特に非同期処理を含むコードを書くときは、処理の流れを正しく理解していないと、予期せぬバグに悩まされることになります。

これからJavaScriptの処理順と実行タイミングについて、一緒に深掘りしていきましょう。

きっとあなたのJavaScriptコーディングに対する理解が深まるはずです。

○同期処理と非同期処理の違い

まずは同期処理と非同期処理の違いから見ていきましょう。

同期処理は、一つの処理が完了するまで次の処理に進まないという特徴があります。

つまり、上から順番に処理が実行されていくのです。

一方、非同期処理では、ある処理の完了を待たずに次の処理を開始することができます。

バックグラウンドで処理が進められるため、メインの処理の流れを妨げずに済むのが大きなメリットですね。

JavaScriptではsetTimeout()やPromise、Async/awaitなどを使うことで、非同期処理を実現できます。

これらを適切に使い分けることが、JavaScriptで効率的なコードを書くコツなのです。

○HTMLとJavaScriptの読み込み順

次に、HTMLとJavaScriptの読み込み順について見ていきましょう。

JavaScriptをHTMLのどこに書くかによって、実行タイミングが変わってくるんです。

一般的には、JavaScriptはHTMLの末尾、タグの直前に記述するのが良いとされています。

なぜかというと、HTMLがすべて読み込まれた後にJavaScriptが実行されるからです。

HTMLの途中でJavaScriptが実行されると、まだ読み込まれていない要素を参照しようとしてエラーになってしまうことがあるんですよね。

ただし、JavaScriptファイルを外部から読み込む場合は、タグ内でも記述できます。

その際は、deferやasync属性を使って読み込みのタイミングを制御しましょう。

deferは文書の解析が完了した後にファイルを実行し、asyncは文書の解析と並行してファイルを読み込みます。

使い分けが大切ですね。

○サンプルコード1:順番に処理を実行する

それでは、実際にコードを見ながら処理順について理解を深めていきましょう。

まずは同期的に処理を実行する例です。

function task1() {
  console.log("タスク1 開始");
  // 何らかの処理
  console.log("タスク1 終了");
}

function task2() {
  console.log("タスク2 開始");
  // 何らかの処理
  console.log("タスク2 終了");
}

console.log("メインタスク開始");
task1();
task2();
console.log("メインタスク終了");

実行結果は次のようになります。

メインタスク開始
タスク1 開始
タスク1 終了 
タスク2 開始
タスク2 終了
メインタスク終了

コードを見ての通り、メインタスクの中でtask1()とtask2()を順番に呼び出しています。

そのため、task1()が完了してからtask2()が実行され、最後にメインタスクが終了します。

これが同期処理の流れですね。

●非同期処理を制御する方法

さて、JavaScriptの非同期処理について深掘りしていきましょう。

非同期処理は強力な武器ですが、使いこなすには少しコツが必要です。

でも大丈夫、一緒に理解を深めていけば、きっとあなたも非同期処理マスターになれるはずです!

○コールバック関数の使い方

非同期処理を制御する伝統的な方法が、コールバック関数の使用です。

コールバック関数とは、ある処理が完了したときに呼び出される関数のことを指します。

例えば、setTimeout()を使ってタイマー処理を行う場合、指定した時間が経過した後にコールバック関数が実行されます。

こんな感じで使います。

console.log("タイマー開始");

setTimeout(function() {
  console.log("2秒経過しました!");
}, 2000);

console.log("タイマー終了");

実行結果は次のようになります。

タイマー開始
タイマー終了
2秒経過しました!

非同期処理であるsetTimeout()の完了を待たずに、次の処理が実行されているのがわかりますね。

コールバック関数を使えば、このようにメインの処理の流れを止めずに非同期処理を実行できます。

ただし、コールバック関数を多用すると、コードの見通しが悪くなる「コールバック地獄」に陥る危険性があります。

そこで登場したのが、Promiseとasync/awaitなのです。

○Promiseを用いた処理の待機

Promiseは、非同期処理の状態を表すオブジェクトです。

Promiseを使えば、非同期処理の成功(resolve)または失敗(reject)を分かりやすく処理できます。

Promiseは次のように使います。

function asyncTask() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log("非同期タスク完了");
      resolve();
    }, 2000);
  });
}

console.log("メインタスク開始");
asyncTask().then(function() {
  console.log("メインタスク終了");
});

実行結果は次のようになります。

メインタスク開始
非同期タスク完了
メインタスク終了

asyncTask()がPromiseを返すことで、非同期処理の完了を.then()で待つことができました。

これにより、非同期処理の流れが明確になり、コードの可読性が向上します。

○サンプルコード2:Promiseで処理を待つ

それでは、もう少し実践的なサンプルコードを見てみましょう。

function task1() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log("タスク1完了");
      resolve();
    }, 2000);
  });
}

function task2() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log("タスク2完了");
      resolve();
    }, 1000);
  });
}

console.log("メインタスク開始");
task1()
  .then(task2)
  .then(function() {
    console.log("メインタスク終了");
  });

実行結果は次の通りです。

メインタスク開始
タスク1完了
タスク2完了
メインタスク終了

task1()とtask2()をPromiseチェーンでつなぐことで、非同期タスクを順番に実行しています。

これにより、コールバック地獄に陥ることなく、可読性の高いコードを書くことができました。

○Async/awaitによる直感的な記述

さらに、ES2017で導入されたasync/awaitを使えば、Promiseをより直感的に記述できます。

async/awaitは、非同期処理を同期処理のように書けるのが特徴です。

次のようなコードを見てみましょう。

async function asyncTask() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log("非同期タスク完了");
      resolve("非同期処理の結果");
    }, 2000);
  });
}

async function main() {
  console.log("メインタスク開始");
  const result = await asyncTask();
  console.log(result);
  console.log("メインタスク終了");
}

main();

実行結果は次の通りです。

メインタスク開始
非同期タスク完了
非同期処理の結果
メインタスク終了

asyncTask()をawaitすることで、Promiseの完了を待ち、その結果を変数resultに代入しています。

これにより、非同期処理を同期的に記述でき、とてもわかりやすいコードになりました。

○サンプルコード3:Async/awaitの基本

最後に、async/awaitを使ったサンプルコードをもう1つ見てみましょう。

async function task1() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log("タスク1完了");
      resolve("タスク1の結果");
    }, 2000);
  });
}

async function task2(result) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log(result);
      console.log("タスク2完了");
      resolve("タスク2の結果");
    }, 1000);
  });
}

async function main() {
  console.log("メインタスク開始");
  const result1 = await task1();
  const result2 = await task2(result1);
  console.log(result2);
  console.log("メインタスク終了");
}

main();

実行結果は次のようになります。

メインタスク開始
タスク1完了
タスク1の結果
タスク2完了 
タスク2の結果
メインタスク終了

非同期処理の結果を次の非同期処理に渡すことで、連続した非同期処理をスッキリと記述できました。

async/awaitを使いこなせば、複雑な非同期処理もシンプルに書けるようになるでしょう。

●Ajax通信における処理順の注意点

JavaScriptの非同期処理の代表格と言えば、Ajax通信ですよね。

Ajaxを使えば、ページ全体を再読み込みすることなく、サーバーとデータをやり取りできます。

でも、その便利さの裏で、処理順に関する落とし穴が潜んでいるんです。

ここからはAjax通信での処理順制御について、しっかり理解を深めていきましょう。

Ajax通信の非同期性に振り回されないために、知っておくべきポイントを押さえていきますよ。

○Ajaxリクエストが完了するまで待つ

まず大切なのは、Ajaxリクエストが完了するまで待つということです。

Ajaxは非同期で通信を行うため、リクエストを送信しても、すぐにレスポンスが返ってくるとは限りません。

例えば、こんなコードがあったとします。

console.log("Ajaxリクエスト開始");

$.ajax({
  url: "/api/data",
  success: function(data) {
    console.log("Ajaxリクエスト完了");
    console.log(data);
  }
});

console.log("次の処理");

実行結果は、こんな感じになるかもしれません。

Ajaxリクエスト開始
次の処理
Ajaxリクエスト完了
{...レスポンスデータ...}

Ajaxリクエストの完了を待たずに次の処理が実行されてしまうのがわかりますね。

これでは、Ajaxで取得したデータを使った処理を正しく行えません。

○サンプルコード4:Ajaxの順次処理

では、Ajaxリクエストが完了してからデータを処理するには、どうすればいいのでしょうか。

1つの方法は、Promiseを使ってAjaxの処理を待つことです。

console.log("処理開始");

function fetchData() {
  return new Promise(function(resolve, reject) {
    $.ajax({
      url: "/api/data",
      success: function(data) {
        resolve(data);
      },
      error: function() {
        reject("Ajaxエラー");
      }
    });
  });
}

fetchData()
  .then(function(data) {
    console.log("Ajaxリクエスト完了");
    console.log(data);
    return fetchData();
  })
  .then(function(data) {
    console.log("2回目のAjaxリクエスト完了");
    console.log(data);
  })
  .catch(function(error) {
    console.log(error);
  })
  .finally(function() {
    console.log("処理終了");
  });

実行結果は、こんな感じになります。

処理開始
Ajaxリクエスト完了
{...レスポンスデータ...}
2回目のAjaxリクエスト完了
{...レスポンスデータ...}
処理終了

Promiseを使うことで、Ajaxリクエストを順番に処理できました。

.then()を使えば、Ajaxリクエストが完了した後の処理をチェーンのようにつなげられるんです。

これなら、取得したデータを使った処理も、意図した順番で実行できますね。

●よくあるエラーと対処法

JavaScriptの処理順制御について理解が深まってきたところで、ちょっと脱線して、よくあるエラーとその対処法について見ていきましょう。

非同期処理を扱っていると、思わぬところでエラーに遭遇することがあります。

でも大丈夫、エラーに立ち向かう勇気さえあれば、必ず解決への道は開けるはずです。

ここでは、処理順に関連したエラーを中心に、その原因と対処法を探っていきます。

きっとあなたの経験にも当てはまるエラーが見つかるでしょう。

○処理が途中で止まってしまう

非同期処理を含むコードを書いていると、処理が途中で止まってしまうことがあります。

特に、コールバック関数を多用している場合、このエラーに遭遇しやすいですね。

処理が途中で止まる主な原因は、エラーが発生したことです。

例えば、undefinedのプロパティにアクセスしようとしたり、存在しない関数を呼び出したりすると、エラーが発生し、処理が中断されてしまいます。

こんなコードがあったとします。

function fetchData(callback) {
  setTimeout(function() {
    // エラーが発生する可能性のある処理
    callback(data);
  }, 1000);
}

fetchData(function(data) {
  console.log(data);
  // 以降の処理は実行されない
});

もしfetchData()内でエラーが発生すると、コールバック関数は呼び出されず、以降の処理は実行されません。

処理が途中で止まらないようにするには、エラーハンドリングを適切に行うことが大切です。

try…catch文を使ってエラーをキャッチし、適切に処理を行いましょう。

○思った順番で処理が実行されない

JavaScriptの非同期処理に慣れていないと、思った順番で処理が実行されないことがあります。

コードを見た目の順番通りに実行されると思い込んでいると、予期せぬ動作に悩まされることになりますね。

非同期処理が絡むと、処理の実行順序が変わることがあります。

例えば、setTimeoutやPromise、Ajaxなどの非同期処理は、一旦メインスレッドから外れて実行されるため、その後に書かれたコードが先に実行されることがあるのです。

先ほどのサンプルコード4を少し変えてみましょう。

console.log("処理開始");

function fetchData() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve("データ");
    }, 1000);
  });
}

console.log("Ajaxリクエスト開始");

fetchData()
  .then(function(data) {
    console.log("Ajaxリクエスト完了");
    console.log(data);
  });

console.log("Ajaxリクエスト完了?");

実行結果は、こんな感じになります。

処理開始
Ajaxリクエスト開始
Ajaxリクエスト完了?
Ajaxリクエスト完了
データ

fetchData()の処理が完了する前に、”Ajaxリクエスト完了?”が出力されています。

非同期処理の完了を待たずに、次の処理が実行されてしまったのです。

処理の実行順序を制御するには、非同期処理の仕組みを理解し、適切に同期処理と組み合わせることが大切です。

Promiseやasync/awaitを使えば、非同期処理を同期的に扱えるため、処理の流れを整理しやすくなりますよ。

○エラーハンドリングの方法

エラーハンドリングを適切に行うことは、JavaScriptの処理順制御においてとても重要です。

エラーが発生しても、アプリケーションが停止しないようにする必要があります。

JavaScriptは、エラーが発生するとその時点で処理が中断されてしまいます。

そのため、エラーハンドリングを行わないと、ユーザーに不便を強いることになりかねません。

特に、非同期処理では、エラーが発生しても気づきにくいため、注意が必要です。

Promiseを使った非同期処理では、.catch()を使ってエラーをキャッチできます。

function fetchData() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      reject("エラーが発生しました");
    }, 1000);
  });
}

fetchData()
  .then(function(data) {
    console.log(data);
  })
  .catch(function(error) {
    console.log(error);
  });

実行結果は、こうなります。

エラーが発生しました

.catch()を使えば、エラーが発生しても、そこでアプリケーションが止まることなく、適切にエラー処理を行えます。

try…catchや.catch()を使って、エラーをキャッチし、適切に処理することが大切です。

エラーが発生してもアプリケーションを停止させないことで、ユーザーの利便性を損なわずに済みますからね。

エラーハンドリングを適切に行うことは、上級JavaScriptエンジニアへの第一歩だと言えるでしょう。

●処理順制御のベストプラクティス

JavaScriptの処理順制御について理解が深まってきたところで、ベストプラクティスについて考えてみましょう。

非同期処理を効果的に使いこなすには、コードの可読性や保守性、パフォーマンスなどを意識する必要があります。

ここからは、JavaScriptのプロとして知っておくべき、処理順制御のベストプラクティスについて深掘りしていきます。

あなたのコードをより良いものにするヒントが、きっと見つかるはずです。

○可読性と保守性を考慮したコード

処理順制御を行う際は、コードの可読性と保守性を考慮することが大切です。

複雑な非同期処理を含むコードは、理解が難しく、バグを生みやすいからです。

コードの可読性が低いと、他の開発者がコードを理解するのに時間がかかります。

また、保守性が低いと、機能の追加や変更が難しくなり、開発効率が下がってしまいます。

例えば、コールバック地獄に陥ったコードは可読性が低く、保守性に問題があると言えます。

function task1(callback) {
  setTimeout(function() {
    console.log("タスク1完了");
    callback();
  }, 1000);
}

function task2(callback) {
  setTimeout(function() {
    console.log("タスク2完了");
    callback();
  }, 1000);
}

function task3(callback) {
  setTimeout(function() {
    console.log("タスク3完了");
    callback();
  }, 1000);
}

task1(function() {
  task2(function() {
    task3(function() {
      console.log("全てのタスクが完了しました");
    });
  });
});

このコードは、非同期タスクを順番に実行していますが、ネストが深くなり、可読性が低くなっています。

可読性と保守性を高めるには、Promiseやasync/awaitを使って、非同期処理を同期的に記述することをおすすめします。

また、関数を細かく分割し、責務を明確にすることも大切ですね。

○サンプルコード6:モジュール化の例

コードをモジュール化することで、可読性と保守性を高められます。

モジュール化によって、関数の責務を明確にし、再利用性を高められるからです。

先ほどのコードをモジュール化してみましょう。

// タスクを定義するモジュール
const taskModule = (() => {
  function task1() {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log("タスク1完了");
        resolve();
      }, 1000);
    });
  }

  function task2() {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log("タスク2完了");
        resolve();
      }, 1000);
    });
  }

  function task3() {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log("タスク3完了");
        resolve();
      }, 1000);
    });
  }

  return { task1, task2, task3 };
})();

// タスクを実行するモジュール
(async () => {
  await taskModule.task1();
  await taskModule.task2();
  await taskModule.task3();
  console.log("全てのタスクが完了しました");
})();

実行結果は、こうなります。

タスク1完了
タスク2完了
タスク3完了
全てのタスクが完了しました

モジュール化によって、タスクの定義と実行を分離できました。

これにより、コードの見通しが良くなり、保守性が向上しています。

モジュール化は、コードの可読性と保守性を高めるための有効な手段です。

JavaScriptでは、即時関数を使ったモジュールパターンや、ES Modulesを使ったモジュール化が一般的です。

適切にモジュール化することで、コードの品質を高めていきましょう。

○サンプルコード7:エラー処理の例

非同期処理を扱う際は、エラー処理を適切に行うことが重要です。

Promise や async/await を使えば、try…catch を使ってエラーをキャッチできます。

Promiseを使ったエラー処理の例を見てみましょう。

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // エラーをシミュレート
      reject(new Error("データの取得に失敗しました"));
    }, 1000);
  });
}

async function main() {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error("エラーが発生しました:", error);
  }
}

main();

実行結果は、こうなります。

エラーが発生しました: Error: データの取得に失敗しました

fetchData()内で発生したエラーを、try…catchを使ってキャッチできました。

エラーメッセージを適切に処理することで、アプリケーションの信頼性を高められます。

非同期処理では、エラーが発生しても気づきにくいことがあります。

そのため、Promiseのrejectionを適切にハンドリングし、try…catchを使ってエラーをキャッチすることが大切です。

エラー処理を丁寧に行うことで、アプリケーションの安定性を高めていきましょう。

○パフォーマンスを意識した非同期処理

非同期処理は、パフォーマンスを向上させるための強力な手段です。

しかし、使い方を誤ると、かえってパフォーマンスを悪化させてしまうこともあります。

非同期処理を多用しすぎると、かえってオーバーヘッドが大きくなり、パフォーマンスが低下する可能性があります。

また、非同期処理の結果を待つために、無駄なウェイトが発生することもあるでしょう。

例えば、次のようなコードは、パフォーマンスの観点から問題があります。

async function fetchData() {
  const response = await fetch("/api/data");
  const data = await response.json();
  return data;
}

async function main() {
  const result1 = await fetchData();
  const result2 = await fetchData();
  const result3 = await fetchData();
  console.log(result1, result2, result3);
}

main();

このコードでは、fetchData()を3回呼び出していますが、それぞれの呼び出しが直列に実行されています。

つまり、1つ目のfetchData()が完了するまで、2つ目のfetchData()は待機状態になるのです。

パフォーマンスを意識した非同期処理を行うには、並列処理を活用することが有効です。

Promise.allを使えば、複数の非同期処理を並列に実行し、全ての処理が完了するまで待機できます。

async function fetchData() {
  const response = await fetch("/api/data");
  const data = await response.json();
  return data;
}

async function main() {
  const [result1, result2, result3] = await Promise.all([
    fetchData(),
    fetchData(),
    fetchData(),
  ]);
  console.log(result1, result2, result3);
}

main();

このように、Promise.allを使って並列処理を行えば、無駄なウェイトを削減し、パフォーマンスを向上させられます。

まとめ

JavaScriptの処理順制御は、効率的で信頼性の高いWebアプリケーション開発に欠かせません。

同期処理と非同期処理の違いを理解し、コールバック関数、Promise、Async/awaitなどを適切に使い分けましょう。

可読性、保守性、パフォーマンスを意識しながら非同期処理を使いこなすことが、上級エンジニアへの第一歩です。

常に学び続ける姿勢を持ち、JavaScriptの処理順制御を自在に操れるエンジニアを目指してください。