jQuery / .ajax() / 同期要求の方法と非同期の代替

jQuery.ajax()のasync: falseは完了まで現在の実行を待たせる同期要求です。ただしブラウザーの主実行を塞いで画面が応答しなくなる場合があるため、新規コードは既定の非同期を使い後続処理を接続します。第一応答後の第二要求という順序も同期化せず実現できます。

async:falseの意味

jQuery.ajax()仕様の既定はtrueです。falseは高速化でなく、完了前に呼び出し後へ進ませない設定です。MDNの同期・非同期要求も画面が応答しなくなる問題を説明しています。

旧同期コードにはsuccess・error・completeオプションを使います。jQueryは1.8からasync:falseとjqXHRのDeferred方式の組み合わせを非推奨にしています。すべてのAjaxや非同期.done()が非推奨という意味ではありません。以下の.then()・.done()・.fail()は非同期代替です。

目的が順序なら、第一応答後に次を開始して要求を返し接続します。独立した並列要求と前の値が必要な要求を区別します。

遅延するローカルサーバーと完全な画面

Python 3、インターネット、新しい練習フォルダーを用意し、UTF-8のajax-server.pyを保存します。一つの出所でファイルと三APIを提供します。/firstはid 7、/secondはparentを使い、/slowはタイムアウト用に長く待ちます。遅延は意図的な実習条件です。

from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import json
import sys
import time

class Handler(SimpleHTTPRequestHandler):
    def do_GET(self):
        url = urlparse(self.path)
        if url.path not in ("/first", "/second", "/slow"):
            return super().do_GET()
        time.sleep(2 if url.path == "/slow" else 0.4)
        if url.path == "/second":
            parent = parse_qs(url.query).get("parent", [""])[0]
            data = {"detail": "Details for " + parent}
        else:
            data = {"id": 7, "name": "Alpha"}
        body = json.dumps(data).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        try:
            self.wfile.write(body)
        except (BrokenPipeError, ConnectionResetError):
            pass

if __name__ == "__main__":
    port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
    ThreadingHTTPServer(("127.0.0.1", port), Handler).serve_forever()

同じ場所へajax-demo.htmlを保存します。Ajaxなしslimでなく公式完全版4.0.0です。作業中は開始操作を無効にし、activeへ実際の進行中jqXHRを保持します。.then()内で第二要求を返すことで後続もその完了を待ちます。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Keep request order without blocking</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; max-width: 760px; margin: 24px auto; padding: 16px; }
    button { font: inherit; margin: 4px; max-width: 100%; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <h1>Sequence asynchronous requests</h1>
  <button type="button" class="start" id="run">Run two requests</button>
  <button type="button" class="start" id="error">First request fails</button>
  <button type="button" class="start" id="timeout">First request times out</button>
  <button type="button" id="cancel" disabled>Cancel active request</button>
  <button type="button" id="reset">Reset</button>
  <p id="heartbeat">heartbeat=0</p>
  <pre id="result" aria-live="polite">Ready</pre>
  <script>
    $(function () {
      let active = null, busy = false, lines = [], ticks = 0;
      setInterval(function () { ticks++; $("#heartbeat").text(`heartbeat=${ticks}`); }, 100);
      function log(line) { lines.push(line); $("#result").text(lines.join("\n")); }
      function run(path, timeout) {
        if (busy) return;
        busy = true; lines = [];
        $(".start, #reset").prop("disabled", true); $("#cancel").prop("disabled", false);
        log("before");
        active = $.ajax({ url: path, dataType: "json", timeout: timeout });
        active.then(function (first) {
          log("first=" + first.name);
          active = $.ajax({ url: "/second", data: { parent: first.id }, dataType: "json", timeout: 1000 });
          return active;
        }).done(function (second) {
          log("second=" + second.detail);
        }).fail(function (xhr, textStatus) {
          log("failed=" + textStatus + "; HTTP=" + xhr.status);
        }).always(function () {
          log("complete"); active = null; busy = false;
          $(".start, #reset").prop("disabled", false); $("#cancel").prop("disabled", true);
        });
        log("returned to caller");
      }
      $("#run").on("click", function () { run("/first", 1000); });
      $("#error").on("click", function () { run("/missing.json", 1000); });
      $("#timeout").on("click", function () { run("/slow", 300); });
      $("#cancel").on("click", function () { if (active) active.abort(); });
      $("#reset").on("click", function () { lines = []; $("#result").text("Ready"); });
    });
  </script>
</body>
</html>

初期結果です。別タイマーのheartbeatは100msごとに増えます。

Ready

練習フォルダーで次を実行します。pythonがなければ導入とパスを確認します。8000使用中ならpython ajax-server.py 8001としブラウザーも合わせます。

python ajax-server.py

実行中にhttp://127.0.0.1:8000/ajax-demo.htmlを開き、file://にはしません。jQueryのコンテンツ配信ネットワーク(CDN)へインターネットも必要です。終了はCtrl+Cです。Windows Edge、3.7.1・4.0.0、Pythonサーバーで実行し、検証ポートだけ空き番号へ変えました。

順序・失敗・中止を確認する

  1. Run two requestsは先にbeforeとreturned to callerを出し、待機中もheartbeatが増えます。
  2. 次はfirst=Alpha、second=Details for 7、completeです。Networkで第一応答後の/second?parent=7を確認します。同時開始ではありません。
  3. First request failsはfailed=error; HTTP=404とcompleteで、第二要求は開始しません。
  4. First request times outは/slowの2秒に対し300ms制限でfailed=timeout; HTTP=0です。0はサーバーのHTTP応答コードではありません。
  5. Cancel active requestは進行中を中止しfailed=abort; HTTP=0です。第一応答後は第二要求をactiveが指します。
  6. 終了後は開始・初期化が再び使えます。Resetは出力をReadyにしheartbeatは続きます。失敗後の正常再試行も確認します。

.always()で成功・失敗ともbusyと操作部を戻します。成功時だけでは失敗後に操作不能になる場合があります。接続オブジェクトと中止可能なjqXHRを区別し、activeを各$.ajax()で更新します。中止はサーバーで実行済みの作業を取り消す意味ではありません。

旧同期コードの順序

サーバー起動中で他要求が終わってから、開発者ツールConsoleで一度実行します。旧async:false確認用で、この/firstだけを使い遅い外部URLに変えません。

console.log("before synchronous request");
setTimeout(function () { console.log("timer after synchronous request"); }, 0);
$.ajax({
  url: "/first",
  dataType: "json",
  async: false,
  success: function (data) { console.log("success=" + data.name); },
  error: function (xhr, textStatus) { console.log("failed=" + textStatus); }
});
console.log("after synchronous request");
before synchronous request
success=Alpha
after synchronous request
timer after synchronous request

successが次行より先で、0msタイマーは同期呼び出しと後続コードの終了後です。0msは割り込み実行ではありません。同期非推奨の警告が出ても隠すことは解決ではありません。

先にLoadingを書いて同期を始めても描画・タイマー・入力処理の機会が不足する場合があります。非同期のheartbeatと比べ、応答待ちと主実行を塞ぐことを区別します。

既存コードを変更するとき

外部変数へ保存して直後に返すコードをtrueだけに変えると準備前に読む場合があります。利用コードも完了側へ移すか要求・Promiseを返して呼び出し元が待つ形にします。.then()内で要求を作っても返さなければ待ちません。

タイムアウト、応答形式不正、通信失敗、中止は別結果です。実用コードは応答構造も検証します。同期へ時間制限や中止設定を足すだけでは主実行のブロックはなくなりません。送信・保存は重複実行の影響に合わせ再試行方針も決めます。