JavaScriptにおける非同期処理の基礎をざっくりまとめる


本記事について

  • ざっくりとJavaScripの非同期処理についてまとめました
  • 本記事は非同期処理関するあれやこれを浅く広く、ざっくりと説明するものです
  • 実際に非同期処理を書く前にはMDNなどでより詳しい書き方を調べる必要があります
  • 詳しい書き方を調べる前に、この記事などでざっくりとイメージを持って置くと頭に入りやすいのではと思います

非同期処理の概要

そもそも同期処理とは

  • プログラムを上から書かれた順番に処理する
  • 例えば以下のコードは上から順番に処理されるため、処理の順番はA→B→C となる
  • 処理順番がわかりやすい!
処理 A
処理 B
処理 C

非同期処理とは

  • プログラムを上から書かれた順番に処理しないことがある
  • 例えば以下のコードは上から順番に処理されずに、処理の順番はA→C→Bとなる
  • 処理順番がわかりにくいことがある
処理 A
setTimeout(()=>{ 処理B }, 1000)
処理 C

非同期処理のメリット

  • 任意の処理を任意のタイミングで実行できる
  • そのため、プログラムの合計処理時間を短くできる、などを期待できる
    • 例えば、DBから取得した情報をもとに情報表示する処理や何かしら別の処理を同時に行う時のイメージは以下の通り

非同期処理の具体例

いくつか非同期処理を実現するための具体例を紹介します。
他にもあるので、詳細はMDNなどを参照ください。

setTimeout(function, delay)

概要

  • delayで指定したミリ秒後にfunction(callback関数)で指定した処理をTask Queueに登録する
    • Task Queueについては後述する
    • 現時点ではJavaScripのやることリストに登録するイメージでとらえてください!
  • 以下のコードは処理Aが完了してから、約1000ミリ秒後に処理Cが完了してる場合に処理Bを実行する
処理 A
setTimeout(()=>{ 処理B }, 1000)
処理 C

具体例

  • 以下のコードはWebサイトの画像を一定時間で順繰りに変更し続ける関数の一部抜き出し
  • いわゆる、カルーセルとかスライダーなどと呼ばれる動作をする
  • 関数autoPlay内のsetTimeoutのfunctionでautoPlayを呼び出すことで一定時間に変更し続ける機能を実現している
 let currentNum = 0;    // targetImageSetのインデックス

 function autoPlay() {
   slideImage.src = targetImageSet[currentNum];  // 表示する画像をcurrentNumに応じて変更する

   setTimeout(() => {
     if (currentNum === targetImageSet.length - 1) {
       currentNum = 0;
     } else {
       currentNum++;
     }
     autoPlay();  // setTimeoutの中でautoPlayを呼び出すことで、delayで指定した時間間隔ごとに同じ処理を繰り返す
   }, 8000);  // delay=8000ms
 }

new Promise(function)

概要

  • 非同期処理で実行したい処理の内容、処理が成功または失敗したときに実行する処理をまとめたオブジェクト
  • 主に以下のような特徴を持つ(他にもたくさんある)
Promiseインスタンスはfunctionの実行状態を持つ
  • Promiseインスタンスはfunctionの実行状態を持つ
    • 状態はpending, fulfilled, rejected の3種類
    • Promiseインスタンスを作成してからfunctionが完了するまではpending状態
    • functionが成功した場合はfulfilled, 失敗した場合はrejectedになる
Promiseインスタンスはfunctionが成功、または失敗したときに実行したい処理を登録できる
  • functionが成功した時に実行したい処理を登録する場合はthenメソッドを使う
  • thenメソッドで登録した処理(図中のthenFunc)はPromiseインスタンスがfulfiledになったら実行される
  • 失敗時の処理を登録するメソッドなど他のメソッドもいくつかある
functionは処理が成功または失敗した時に呼び出す関数名を引数に取る
  • 通常、成功時はresolveで失敗時はrejectという名前にする
  • 上記の関数に引数を渡して実行することでfunctionの結果を引数に確定できる

上記三つの特徴をコードで示すとこんな感じ。
およそ1000ミリ秒後にpromiseSampleがfulfiledになり、promiseSampleのthenメソッドで登録した処理が実行される。

// Promiseインスタンスを作成して変数promiseSampleに代入する
// 引数のfunctionの第一引数(fulfiledにするときに呼ぶ関数名)はresolveとする
const promiseSample = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolveが呼ばれるとpromiseSampleの状態がfulfiledになる
    // resolveの引数である'value'はthenで登録した処理で使用することができる
    resolve('value');
  }, 1000);
});

// promiseSampleがfulfiledになったら実行する処理を登録する
promiseSample.then((result) => {
  // 変数resultは'value'が格納されている
  // 好きな処理を登録できる
});

async / await

概要

  • Promiseをより分かりやすく利用できる構文
async
  • asyncを宣言して定義する関数は以下の動作をする
    • 関数を実行する際にawaitを宣言できるようになる
    • Promiseを返すようになるため以下のような機能を使える
      • thenなどのPromiseのインスタンスメソッドを利用できる
      • 関数内でreturnをした値でresolveされる
await
  • awaitを宣言して実行する関数は以下の動作をする
    • 呼び出す関数がPromiseを返す場合、そのPromiseがfulfilledまたはrejectedになるまで処理を中断する
    • Promiseがfulfilledまたはrejectedになったら処理を再開する
function promiseFunc(){
  return new Promise((resolve)=>{
    setTimeout(() => {
      resolve('resolved!');
    }, 1000);
  })
}

// asyncを宣言しているため関数定義内でawaitを宣言できるようになる
async function asyncFunc(){
  // awaitを宣言してPromiseが返る関数を実行している
  // Promiseがfulfiledになるまで次の処理(return)を実行しない
  const result = await promiseFunc();
  return result;
}

// asyncを宣言して定義した関数なので、Promiseが返ってくる
// Promiseが返ってくるのでthenメソッドを使用できる
asyncFunc().then((value)=>{
  console.log(value)
});

TaskとMicro Task

非同期処理において、実行順序を正しく把握するためにはTaskとMicro Taskの概念をイメージできると良い。setTimeoutは指定したミリ秒後に必ずしもcallbackを実行するわけではないことなどがわかる。

Task

  • JavaScriptは処理するTaskが来るのを待つ
  • すでにTaskを処理している最中にTaskが来るとTask Queueに登録する
  • Task Queueは古い順から処理される
  • Taskの例:
    • scriptタグの処理内容
    • setTimeoutのコールバック
    • mouseoverのイベントハンドラ … など

Micro Task

  • 一つのTaskが処理された後にMicro Task QueueにMicro Taskが存在する場合に実行される
  • 複数のMicro Taskが存在する場合、古い順に処理をして全て完了したら次のTaskが処理される
  • 主にPromiseによって生成される

具体例

  • setTimeoutのコールバックは必ずしも指定したミリ秒後に実行されるわけではない
  • 下のコードは以下の順でアラート表示される
    • Task1
    • Micro Task1
    • Micro Task2
    • Task2
setTimeout(() => alert("Task2"),0);

Promise.resolve()
  .then(() => alert("Micro Task1"));

Promise.resolve()
  .then(() => alert("Micro Task2"));

alert("Task1");

まとめ

  • 非同期処理で書くことで、より効率的に処理を実行することができる
  • Promiseで非同期処理で実行したい処理の内容、処理が成功または失敗したときに実行する処理をまとめることができる
  • async/awaitでPromiseをより分かりやすく書くことができる
  • 非同期処理の処理順序はTaskとMicroTaskの関係を理解することでイメージしやすくなる