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

# TL; DR

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

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

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

### 非同期のリクエスト

#### 基本構文
```javascript
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.status`は`200`かどうかも確認なければならないです。

```javascript
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にリクエストしたい場合、コードが重複される可能性があります。重複したコードを避けるため、関数に入れてリファクタリングします。

#### 関数でリファクタリング

```javascript
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でリファクタリング
```javascript
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));
```

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

```javascript
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よりもっと柔軟な操作が可能です。

### 非同期のリクエスト

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

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

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

悪い例：

```javascript
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`で全てリクエストのエラーをキャッチできます。

```javascript
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

### 非同期のリクエスト

```javascript
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`を使わなと戻ったデータを使えないです。

### 複数の非同期のリクエスト
```javascript
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 を返します。

```javascript
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](https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest/readyState)

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