JavaScript非同期処理

JavaScript の 非同期処理

1.非同期処理とは




メインスレッドと同期をとらずに行う実装方法です。そのままですが非同期で処理を行います。

ブラウザで、スレッドという表現がピンときませんが、画面をレンダリングする時、表示する時になると思います。開くのが遅いサイトありますよね。例えば、ブラウザ表示時に、非同期処理を使わず全部のデータの表示をしようとするサイトの場合、全てのデータの表示が10秒かかるなら、そのサイトが見えるようになるのも、10秒後になるという事になります。その10秒のうち、動画(2秒が4本)やデータ抽出(2秒)なら、ブラウザを表示させてから、非同期で取得した方が、10秒待たずとも、もしかしたら、最短2秒で何かしら見れるかも。そういうための機能です。


JavaScript は シングルスレッド (メインスレッドのみ)

処理が A、B、C あれば、ブラウザ表示は A + B + C の時間が必要になる

この A、B、C をメインスレッドと同期させず処理する方法

これが 非同期処理 です。


ということで、文章や説明を長々細かく書きまくっても逆に分からなくなるので
端的にまとめてみました。では、どのように実装するのかというところに、即入りたいと思います。





2.ES2015(ES6) 以前と以降




タイトルどおり、ES2015(ES6) 以前と以降でかなりコーディング方法の変化がありました。
非同期処理はコールバック関数を使っていたものが非推奨になり、promise / async / await を
使うようになりました。既存のシステム改修などで古い実装も出てくることがあると思いますが
基本的には、旧コードは使わないようにしましょう。

しかし新規開発が当然ではありません。5~10年前のプログラムなどいくらでもあります。
ですので、把握はしておいた方が良いでしょう。やっていれば絶対出会うので。

ということで、ES2015(ES6) 以前と以降 が良い感じでまとめられているサイト。

JavaScript ES5以前とES6(ES2015)以降の違いについて





3.非同期処理 Promise




現在の非同期APIの主流となっているものです。ES2015(ES6)以前はコールバック関数で実装していたようです。
しかし以下のような問題で、今はほとんど使われなくなっているようです。ので、古いものを学ぶのは最近ちょっと
モチベーションが上がらなくなっていまして・・・(笑) 新しい方から勉強します。

コールバックの中でコールバックを呼び出さなければならないので、doOperation()関数が深く入れ子になってしまい、読むのもデバッグするのも大変になってしまいます。これは「コールバック地獄」とか「運命のピラミッド」(インデントがピラミッドを横に並べたように見えるから)と呼ばれることもあります。

このようにコールバックを入れ子にすると、エラー処理もとても難しくなります。最上位のレベルで一度だけエラー処理をするのではなく、「ピラミッド」の各レベルでエラー処理をしなければならないことがよくあります。

このような理由から、現代の非同期 API のほとんどはコールバックを使用しません。その代わり、 JavaScript で非同期プログラミングの基礎となるのはプロミス (Promise) であり、これが次の記事の主題となります。

非同期 JavaScript 入門 – ウェブ開発を学ぶ | MDN (mozilla.org)


・Promise の状態


Promiseには3つの状態があります。

pending非同期処理の実行中の状態を表す
fulfilled非同期処理が正常終了した状態を表す
rejected非同期処理が異常終了した状態を表す


・Promise の 書き方

・ 構文構成
new Promise((resolve, reject) => {
  // 非同期処理の開始

  if (/* 正常に処理が終了した場合 */) {
    resolve(/* 成功時の値 */);
  } else {
    reject(/* 失敗時のエラー */);
  }

})
  .then((result) => {
    // resolveが実行された場合の処理
    // 前のPromiseでresolveされた値(result)を使用した処理など
    return /* 次のPromiseで使用する値 */;
  })
  .then((nextResult) => {
    // 前のthenメソッドで返された値(nextResult)を使用して処理
    // さらにPromiseを追加する場合は、このようにチェーンで繋げることができる
  })
  .catch((error) => {
    // rejectが実行された場合の処理
    // エラーハンドリングや、Promiseチェーンで途中で発生したエラーの処理など
  })
  .finally(() => {
    // Promiseの処理が完了した場合の共通の処理
    // 成功でも失敗でも実行される
  });
・ 説明
Promiseのコンストラクターの引数には、非同期処理が含まれる関数を指定します。

この関数は、引数としてresolverejectを受け取ります。
非同期処理が完了した場合には、resolveメソッドを呼び出して成功時の値を返します。
非同期処理が失敗した場合には、rejectメソッドを呼び出してエラーを返します。

thenメソッドは、前のPromiseresolveされた場合に実行されます。
thenメソッドの引数には、前のPromiseresolveされた値を受け取る関数を指定します。この関数の戻り値が、次のPromiseで使用されます。thenメソッドは、Promiseチェーンの途中に複数回呼び出すことができます。

catchメソッドは、前のPromiserejectされた場合に実行されます。
catchメソッドの引数には、エラーハンドリングを行う関数を指定します。catchメソッドは、Promiseチェーンの途中に1回だけ呼び出すことができます。

finallyメソッドは、前のPromiseの処理が完了した場合に実行されます。finallyメソッドの引数には、前のPromiseで返された値を使用しない場合に実行する関数を指定します。finallyメソッドは、Promiseチェーンの途中に1回だけ呼び出すことができます。

・Promise の 引数 ( resolve, reject )


Promiseの状態が、fulfilledになったら resolve が実行されます。
resolve が実行された場合は、thenメソッド内部が実行されます。
thenメソッド内部のコールバック関数には、resolve実行時の実引数が渡されます。

<div id="divSample03"></div>
<button id="btnSample03">3.非同期Pomise</button>
const divSample03 = document.querySelector("#divSample03"); // div オブジェクト
const btnSample03 = document.querySelector("#btnSample03"); // btn オブジェクト

const getJSONData = () => {
  return new Promise((resolve, reject) => {
    fetch("https://jsonplaceholder.typicode.com/todos/1")  //※(1)
      .then(response => response.json())
      .then(json => resolve(json))
      .catch(error => reject(error));
  });
}

btnSample03.addEventListener("click", () => {
  getJSONData()
  .then(jsonData => {
    console.log(jsonData);
    divSample03.innerText += "userId:" + jsonData.userId + "\n";
    return jsonData;
  })
  .then(jsonData => {
    console.log(jsonData.userId);
    divSample03.innerText += "id:" + jsonData.id + "\n";
    return jsonData;
  })
  .then(jsonData => {
    console.log(jsonData.title);
    divSample03.innerText += "title:" + jsonData.title + "\n";
    return jsonData;
  })
  .catch(error => {
    console.error(error);
    divSample03.innerText = error;
  })
  .finally(() => {
    console.log('Promise finished!');
    divSample03.innerText += 'Promise finished!';
  });
});
#divSample03 { 
  width: 400px; height: 130px; 
  border: 1px solid black;
  overflow-wrap: break-word;
  margin:10px;
}
#btnSample03 {
  margin-left:10px;
}

※(1)
fetch()メソッドは、Promiseオブジェクトを返します。このPromiseオブジェクトは、データの取得が完了した後に解決されます。awaitキーワードを使用すると、Promiseオブジェクトが解決されるまで、関数の処理が一時停止されます。そのため、fetch()メソッドを含む非同期関数内では、明示的にPromiseを返す必要はありません。







4.非同期処理 async/await



JavaScriptの async/await は、非同期処理のコードをよりシンプルかつ読みやすくするための構文です。通常の非同期処理コードでは、コールバック関数やPromiseチェーンを使用して、処理の完了を待たずにコードが進むことができます。しかし、async/awaitを使用すると、Promiseオブジェクトを返す非同期処理の関数を同期的に実行できるようになります。以下は、async/awaitの例です。

<div id="divSample04"></div>
<button id="btnSample04">4.非同期 async/await</button>
#divSample04 { 
  width: 400px; height: 130px; 
  border: 1px solid black;
  overflow-wrap: break-word;
  margin:10px;
}
#btnSample04 {
  margin-left:10px;
}
const divSample04 = document.querySelector("#divSample04"); // div オブジェクト
const btnSample04 = document.querySelector("#btnSample04"); // btn オブジェクト

async function getData() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const data = await response.json();
  divSample04.textContent = data.title; // div のテキストを設定
  console.log(data);
}

btnSample04.addEventListener("click", getData);


JavaScriptの非同期処理には、コールバック、Promise、そしてasync/awaitなどの方法があります。async/awaitはPromiseに基づいており、Promiseが使われる場合にも、async/awaitを使用できます。以下は、async/awaitが必要になる場合の一例です。

  1. 非同期処理をシンプルにする async/awaitは、非同期処理の書き方をシンプルにするために使用されます。Promiseチェーンは非常に複雑になることがあり、async/awaitを使用すると、Promiseチェーンを書く必要がなく、非同期処理をより直感的に理解できます。
  2. 複数の非同期処理を同時に行う 複数の非同期処理を同時に実行する場合、Promise.all()を使用して、すべての非同期処理が完了するのを待つことができます。このPromise.all()は、Promiseオブジェクトの配列を受け取り、それらのすべてが完了するまで待ちます。async/awaitは、複数の非同期処理をより簡単に同時に実行するために使用されます。
  3. 非同期処理のエラーハンドリング 非同期処理では、エラーハンドリングが非常に重要です。Promiseチェーンを使用する場合、エラーハンドリングが非常に複雑になることがあります。async/awaitを使用すると、try-catchブロックを使用して、非常にシンプルにエラーハンドリングできます。
  4. 非同期処理の実行順序を制御する async/awaitは、非同期処理の実行順序を制御するためにも使用できます。awaitは、その行の非同期処理が完了するまで、次の行に進まないため、非同期処理の実行順序を制御するのに役立ちます。

以上のように、async/awaitは、非同期処理をシンプルにするために使用されます。非同期処理が多い場合や、非同期処理のエラーハンドリングが必要な場合には、async/awaitを使用することが推奨されます。

え・・・もうちょっとわかりやすく頼みたいなぁ・・・
動作確認しながら、またサンプルつくるかぁ・・・


・Async/Awaitを使用する場合


function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const users = {
        1: { name: 'John', age: 32 },
        2: { name: 'Sarah', age: 25 },
        3: { name: 'Kate', age: 28 }
      };
      if (users[userId]) {
        resolve(users[userId]);
      } else {
        reject('User not found');
      }
    }, 1000);
  });
}

// function に async をつけて、非同期関数に await をつければいいので簡素化できる。
async function getUserData(userId) {  
  try {
    const userData = await fetchUserData(userId);
    console.log(userData);
  } catch (error) {
    console.log(error);
  }
}

getUserData(1); // logs { name: 'John', age: 32 }
getUserData(4); // logs 'User not found'

・Async/Awaitを使用しない場合


function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const users = {
        1: { name: 'John', age: 32 },
        2: { name: 'Sarah', age: 25 },
        3: { name: 'Kate', age: 28 }
      };
      if (users[userId]) {
        resolve(users[userId]);
      } else {
        reject('User not found');
      }
    }, 1000);
  });
}

function getUserData(userId) {
  fetchUserData(userId)    // Promise の構成で書くことになるので、実装が多くなる。
    .then((userData) => {
      console.log(userData);
    })
    .catch((error) => {
      console.log(error);
    });
}

getUserData(1); // logs { name: 'John', age: 32 }
getUserData(4); // logs 'User not found'





5.サンプルプログラム




・ サンプルプログラム


promise / async / await をさっそく使ってサンプルプログラムを作成した。



◇ HTML

<div id="async-txt"/>
<button id="start-btn">非同期サンプル</button>

◇ CSS

#async-txt { 
  width: 200px; height: 100px; 
  border: 1px solid black;
  overflow-wrap: break-word;
  margin:10px;
}
#start-btn {
  margin-left:10px;
}

◇ JavaScript


const asyncDiv = document.querySelector("#async-txt"); // div オブジェクト
const startBtn = document.querySelector("#start-btn"); // btn オブジェクト

const startasync = async () => {

  try {
    const startTime = Date.now();
    const endTime = startTime + 5000; // 5秒間
    let text = "";
    while (Date.now() < endTime) {

      // aからzまでのランダムな英文字
      const randomChar = String.fromCharCode(97 + Math.floor(Math.random() * 26)); 
      text += randomChar;
      asyncDiv.innerText = text;
      await new Promise(resolve => setTimeout(resolve, 50)); // 0.5秒間隔で1文字打つ
    }
  } catch (error) {
    console.error(error);
  }
};

startBtn.addEventListener("click", startasync);