Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

【JavaScript】非同期処理と複数非同期処理の実装方法まとめ

TL; DR

非同期処理について、たくさんのいいリソースや記事等がありますので、本記事は単刀直入に実装例から見てみましょう。
非同期処理の基礎概念に興味がある方はこちらの記事をご参考ください。
今回はXMLHttpRequest, Fetch API, Async/Awaitについて、それぞれの実装例と複数の非同期処理の方法を紹介します。(複数の非同期処理向けのPromise Allも記述しています。)
また、HTTPリクエストをシミュレートするためにJSONPlaceholderのエンドポイントを使っています。

XMLHttpRequest (XHR)

現在はよりモダンなFetch APIがあるため、XMLHttpRequestを若干非推奨としていますが、歴史的な理由により、例え既存のスクリプトをサポートする必要がある場合等、そのままXMLHttpRequestが使われている場合もあります。

リクエストをするためには、4 つのステップが必要です。
1. XMLHttpRequestを作成します
2. リクエストを初期化します
3. リクエストを送ります
4. (イベントをリッスン)レスポンスに対する処理します

非同期のリクエスト

基本構文

const request = new XMLHttpRequest();
request.open('GET', 'https://jsonplaceholder.typicode.com/users');
request.send();
request.addEventListener('load', () => {
  if (request.status !== 200) console.log('error');
  const data = JSON.parse(request.responseText);
  console.log(data);
});

loadイベント以外はreadystatechangeイベントも使えます。
Ready Stateの値について、以下の表を参照してください。

状態 説明
0 UNSENT open()まだ呼ばれていない。
1 OPENED open()が呼び出し済み。
2 HEADERS_RECEIVED レスポンスヘッダを受け取った。
3 LOADING レスポンスはロード中。
4 DONE リクエスト完了。

ここで、一つ注意点があります。
URLに誤りがある場合、リクエストはそのまま実行されます。つまり、リクエスト完了したらステータス 4 になります。なので、request.status200かどうかも確認なければならないです。

const request = new XMLHttpRequest();
request.open('GET', 'https://jsonplaceholder.typicode.com/users');
request.send();
request.addEventListener('readystatechange', () => {
  if (request.readyState !== 4) return;
  if (request.status !== 200) console.log('error');
  // request.readyState === 4 && request.status === 200 場合
  const data = JSON.parse(request.responseText);
  console.log(data);
});

これで、リクエストができましたが、もし他のURLにリクエストしたい場合、コードが重複される可能性があります。重複したコードを避けるため、関数に入れてリファクタリングします。

関数でリファクタリング

const getUsers = (url, callback) => {
  const request = new XMLHttpRequest();
  request.addEventListener('readystatechange', () => {
      if (request.readyState !== 4) return;
      if (request.status !== 200) callback('error', undefined);
      const data = JSON.parse(request.responseText);
      callback(undefined, data);
  });
  request.open('GET', url);
  request.send();
};

getUsers('https://jsonplaceholder.typicode.com/users', (err, data) => {
  if (err) console.log(err);
  console.log(data);
});

ひとつ注意すべき点として、このコードで複数の非同期処理を実行する時にコールバック地獄になるかもしれませんので、以下のようなPromiseを使って、複数の非同期処理のためリファクタリングします。

Promiseでリファクタリング

const getRequest = (url) => {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest();
    request.addEventListener('readystatechange', () => {
      if (request.readyState !== 4) return;
      if (request.status !== 200) reject('error');
      const data = JSON.parse(request.responseText);
      resolve(data);
    });
    request.open('GET', url);
    request.send();
  });
};

getRequest('https://jsonplaceholder.typicode.com/users')
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

複数の非同期のリクエスト

const getRequest = (url) => {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest();
    request.addEventListener('readystatechange', () => {
      if (request.readyState !== 4) return;
      if (request.status !== 200) reject('error');
      const data = JSON.parse(request.responseText);
      resolve(data);
    });
    request.open('GET', url);
    request.send();
  });
};

getRequest('https://jsonplaceholder.typicode.com/users')
  .then((data) => {
    console.log('Request 1: Users', data);
    return getRequest('https://jsonplaceholder.typicode.com/posts');
  })
  .then((data) => {
    console.log('Request 2: Posts', data);
    return getRequest('https://jsonplaceholder.typicode.com/comments');
  })
  .then((data) => console.log('Request 3: Comments', data))
  .catch((err) => console.log(err));

FetchAPI

Fetch API は XMLHttpRequestよりもっと柔軟な操作が可能です。

非同期のリクエスト

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

複数の非同期のリクエスト

複数のリクエストと言ったら、多分直感的に以下のようなネストする事がなりますが、ネストが深くなると構造が把握しづらくなります。
以下の例は三つのリクエストをネストしたサンプルコードです。

悪い例:

fetch('https://jsonplaceholder.typicode.com/users')
  .then((userResponse) => userResponse.json())
  .then((userData) => {
    console.log(userData);
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then((postResponse) => postResponse.json())
      .then((postData) => {
        console.log(postData);
        fetch('https://jsonplaceholder.typicode.com/comments')
          .then((commentResponse) => commentResponse.json())
          .then((data) => console.log(data))
          .catch((commentData) => console.log(commentData));
      })
      .catch((postError) => console.log(postError));
  })
  .catch((userError) => console.log(userError));

ネストを避けるため、2回目と3回目のfetch(url)前にreturnをすると、以下のことができます。
1. fetch(url)にいるコードブロックはPromiseとして返しますので、.thenメソッドチェーンを使用できます。
2. 一つのcatchで全てリクエストのエラーをキャッチできます。

fetch('https://jsonplaceholder.typicode.com/users')
  .then((userResponse) => userResponse.json())
  .then((userData) => {
    console.log(userData);
    return fetch('https://jsonplaceholder.typicode.com/posts');
  })
  .then((postResponse) => postResponse.json())
  .then((postData) => {
    console.log(postData);
    return fetch('https://jsonplaceholder.typicode.com/comments');
  })
  .then((commentResponse) => commentResponse.json())
  .then((commentData) => console.log(commentData))
  .catch((err) => console.log(err));

Async/Await + Fetch APIを使えば、もっと簡潔なコードを書けます。

Async/Await + Fetch API

非同期のリクエスト

const getRequest = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  if (response.status !== 200) throw new Error('Failed to fetch');
  const data = await response.json();
  return data;
};

getRequest()
  .then((data) => console.log(data))
  .catch((err) => console.log(err.message));

非同期関数の返す値はまだPromiseですので、.thenを使わなと戻ったデータを使えないです。

複数の非同期のリクエスト

const getRequest = async () => {
  const checkStatus = (res) => {
    if (res.status !== 200) throw new Error('Failed to fetch');
  };

  const userResponse = await fetch('https://jsonplaceholder.typicode.com/users');
  checkStatus(userResponse);
  const userData = await userResponse.json();
  const postResponse = await fetch('https://jsonplaceholder.typicode.com/posts');
  checkStatus(postResponse);
  const postData = await postResponse.json();
  const commentResponse = await fetch('https://jsonplaceholder.typicode.com/comments');
  checkStatus(commentResponse);
  const commentData = await commentResponse.json();
  return { userData, postData, commentData };
};

getRequest()
  .then((data) => console.log(data))
  .catch((err) => console.log(err.message));

Async/Await + Fetch APIを使うと、コードの可読性を高められます。

Promise All

Promise.all()メソッドは複数の非同期のリクエストをすることができます、そしてまとめて単一の Promise を返します。

const getRequest = async (url) => {
  const response = await fetch(url);
  if (response.status !== 200) throw new Error('Failed to fetch');
  return response.json();
};

const userPromise = getRequest('https://jsonplaceholder.typicode.com/users');
const postPromise = getRequest('https://jsonplaceholder.typicode.com/posts');
const commentPromise = getRequest('https://jsonplaceholder.typicode.com/comments');

Promise.all([userPromise, postPromise, commentPromise])
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

参考文献

MDN - Ready State

ここまでお読みいただきありがとうございました。
日本語まだ勉強中ですので、文法が多少おかしいと思います。
もし文法ミスや誤字がありましたら、編集リクエストでご意見いただければと思います。

chimeiwang
2019年9月より、勉強を始まりました。 問題解決とアウトプットできるエンジニアを目指して、取り組んでいきます。 Twitter - @chimeiW
https://wangchimei.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away