Javascript Ajax 非同期処理を含む繰り返し処理での実行順序について

はじめに

Javascriptで非同期処理を実装する場合、
何も対策を行わず実装してしまうと思いもよらない動作をしてしまうことがあります。

今回はその例と対策を紹介します。

前提条件

HTML

以下のような3つのプルダウンを設置します

言語
<select name="sel0" id="sel0">
  <option value="0">--選択して下さい--</option>
  <option value="1">日本語</option>
  <option value="2">English</option>
</select>

スポーツ
<select name="sel1" id="sel1"></select>
食べ物
<select name="sel2" id="sel2"></select>

Ajax

以下のようなレスポンスが返ってくるAPIを作成します。

//ajax_url1
// param: id => 1
{options: [[1,'サッカー'], [2,'野球'].....]}
// param: id => 2
{options: [[1,'Soccer'], [2,'Baseball'].....]}

// ajax_url2
// param: id => 1
{options: [[1,'かけうどん'], [2,'ぶっかけうどん'].....]}

// param: id => 2
{options: [[1,'Udon Noodles'], [2,'Bukkake Noodles'].....]}

// by google翻訳

期待する結果

今回は、
「言語」のchangeイベントで「スポーツ」、「食べ物」に言語に適したoptionをセットする
を目的とします。(いやそれどこで使うねんというツッコミは野暮)

言語で「日本語」を選択した場合の期待する結果は以下です。

<select name="sel0" id="sel0">
  <option value="0">--選択して下さい--</option>
  <option value="1">日本語</option>
  <option value="2">English</option>
</select>

スポーツ
<select name="sel1" id="sel1">
  <option value="1">サッカー</option>
  <option value="2">野球</option></select>
食べ物
<select name="sel2" id="sel2">
  <option value="1">かけうどん</option>
  <option value="2">ぶっかけうどん</option>
</select>

それではJS部分を実装してみましょう。

実行順序を考慮していない実装

実行順序のことを全く頭に入れずに実装します。

Javascript

$(document).on('change', '#sel0', function() {
  var   val = $(this).val(); // 「言語」で選択した値  
  for(i = 0 ; i < 2 ; i++){
    var select_elm = $('#sel' + i); // selectエレメント
    $.ajax({
      type: 'GET',
      url: 'ajax_url' + i,
      data: { id: val },
      success: function(result) {
        // selectエレメントにoptionを追加
        $.each(result.options, function() {
          select_elm.append($('<option>').val(this[1]).text(this[0]));
        });
      }
    });
  }
});

いかがでしょうか?
正常に動作しそうじゃないですか?

結果

しかし、このような実装だと正しく動作したりしなかったりするのです。
Aさんが動かすと正常に動作したり、
Bさんが動かすと以下のような結果になることもあります。

<select name="sel0" id="sel0">
  <option value="0">--選択して下さい--</option>
  <option value="1">日本語</option>
  <option value="2">English</option>
</select>

スポーツ
<select name="sel1" id="sel1"></select>
食べ物
<select name="sel2" id="sel2">
  <option value="1">かけうどん</option>
  <option value="2">ぶっかけうどん</option>
</select>

うそっしょ?

なぜそのようなことが起きるのか

JSのコードはなんの対応もなしだとAjaxのレスポンスを待ってくれません。
forだけが先に回ってajax_url1(スポーツ)のレスポンスが返ってくる頃にはselect_elmにはsel2のエレメント(食べ物)が代入されているためこのような事象が起こるのです。

実行順序を考慮した実装

クールではないのですが以下のように対策できます。

Ajax

//ajax_url1
// param: id => 1, select_id => 1
{options: [[1,'サッカー'], [2,'野球'].....], select_id: 1}
// param: id => 2, select_id => 1
{options: [[1,'Soccer'], [2,'Baseball'].....], select_id: 1}

// ajax_url2
// param: id => 1, select_id => 2
{options: [[1,'かけうどん'], [2,'ぶっかけうどん'].....], select_id: 2}

// param: id => 2, select_id => 2
{options: [[1,'Udon Noodles'], [2,'Bukkake Noodles'].....], select_id: 2}

// by google翻訳

Javascript

$(document).on('change', '#sel0', function() {
  var   val = $(this).val(); // 「言語」で選択した値  
  for(i = 0 ; i < 2 ; i++){
    $.ajax({
      type: 'GET',
      url: 'ajax_url' + i,
      data: { id: val, select_id: i },
      success: function(result) {
        var select_elm = $('#sel' + result.select_id)  // selectエレメント
        // selectエレメントにoptionを追加
        $.each(result.options, function() {
          select_elm.append($('<option>').val(this[1]).text(this[0]));
        });
      }
    });
  }
});

強引な実装ですが、
appendするselectエレメントをAjaxのsuccess内で定義することで対策ができます。

まとめ

JSでAjaxを使用する時は、実行順序を気にする。(小並感)

気になる

以下の機能を使うともっとクールな実装ができそうです。
興味がある方がググってどうぞ!

  • jQuery.Deferred
  • .when().done()

ブログ記事で見たい方はこちら → Javascript Ajax 非同期処理を含む繰り返し処理での実行順序について

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.