JavaScriptの同期処理と非同期処理、Promiseの仕組み・使い方をわかりやすく解説(then, catch, all, race, finaly)

Promise-Basic

こんにちは、フロントエンドエンジニアのまさにょんです。

今回は、JavaScriptのスレッドと同期処理と非同期処理、Promiseの仕組み・使い方(then, catch, all, race, finaly)について、をわかりやすく解説していきます。

JavaScriptのスレッドと同期処理と非同期処理とは?

JavaScriptを学ぶ上で、重要な概念に「非同期処理」があります。

今回のテーマは、この非同期処理の意味を理解した上で、その実装方法であるPromiseについて学習していきます。

そして、「非同期処理」を正しく理解するために、「スレッド」「同期処理」についても解説します。

プログラムにおけるスレッド(Thread)とは?

スレッド(Thread)とは、一連のプログラムの実行の流れ(実行フロー)のことを言います。

次の記事のスレッドの説明がわかりやすいので、引用します。

コンピュータのプログラムは、基本的に1行ずつコードが実行されながら動作する。

通常、分岐やループがあっても、プログラム全体は1つの流れになっている。

このような一連のプログラムの流れを「スレッド」(Thread:「糸」などの意味)と呼び、

1つのスレッドだけからなるプログラムを「シングルスレッドなプログラム」という。

たいていのプログラミングでは1つの処理の流れを記述するが、

このようなプログラムはシングルスレッドなプログラムに該当する。

一方、プログラムによっては、処理効率を上げるなどの目的で、複数の処理を並行して行うことができる。

つまり、1つのプログラムで複数のスレッドを同時に実行することができるのである。

このようなプログラムを「マルチスレッド・プログラム」という。

プログラムのコード上では、複数個所が同時に実行されている状態となる。

引用元: 難解なマルチスレッド・プログラミングを基礎から解説

上記の引用にあるように、プログラムは、基本的に上から記述された順に、1行ずつ順番に、実行されていきます。

それは、条件分岐やループ処理、関数呼び出しがあっても、そのプログラムの実行フローは、一連の流れ・手順としてプログラムされているわけです。

このような一連のプログラムの実行の流れ(実行フロー)をスレッド(Thread)と呼びます。

ここで、JavaScriptの話に戻ります。

JavaScriptはシングルスレッドのプログラミング言語で、実行中のスレッドを1つしか持ちません。

つまり、JavaScriptコードは1つのスレッドで実行され、基本的には、実行中のタスクが完了するまで他のタスクは実行されないという特徴を持ちます。

このJavaScriptの「シングルスレッド」という特徴が「非同期処理」を必須にする原因なのです。

同期処理とは?

同期処理とは、コードの実行において、1つの処理タスクの実行が完了してから、次の処理に進む仕組み・処理フローのことを指します。

つまり、実行中の1つの処理が終わるまで、次の処理は実行されないという動作をします。

JavaScriptはシングルスレッドのプログラミング言語であり、基本的に1つのスレッドでしかプログラムを実行できません。

なので、すべて同期的に処理をしている場合、1つの重い処理をしているとそれが終わるまで、他の処理が実行できずにブロックされるというデメリットがあります。

例えば、Webフロント画面で、同期処理だけしかできなかったら、重い処理に入った途端、フリーズして他の処理を受け付けない状態になってしまいます。

非同期処理とは?

非同期処理とは、処理中のタスクの実行が完了しなくても、次の処理タスクに移行して、処理中のタスクはバックグラウンドで処理する仕組み・処理フローのことです。

つまり、非同期処理として設定されたタスクは、処理を実行する際に、処理の完了を待たずにバックグラウンド処理になり、他の処理が実行されるわけです。

先述の同期処理の重い処理によるブロック・フリーズ問題などのデメリットを解決するために、JavaScriptでは非同期処理が提供されています。

非同期処理では、処理の順番は決まっておらず、バックグラウンドで処理が進み、終わった順に、その処理結果が反映されていきます。

スレッド・同期処理・非同期処理まとめ

ここまでの概念をまとめておきます。

  1. スレッド(Thread)とは、一連のプログラムの実行の流れ(実行フロー)のこと。
    • JavaScriptはシングルスレッドのプログラミング言語で、実行中のスレッドは、1つだけ。
  2. 同期処理は、1つの処理タスクの実行が完了してから、次の処理に移行する処理の仕組みのことです。
    • シングルスレッドで、同期処理をすると重い処理の際に、他の処理が動けないままになる。
  3. 非同期処理は、1つの処理タスクの実行完了を待たずに、次の処理に移行する処理の仕組みのことです。
    • 非同期処理として設定されたタスクは、処理を実行する際に、処理の完了を待たずにバックグラウンド処理になる。
    • 同期処理のデメリットを克服するための仕組み。
    • 重い処理などバックグラウンドで実行したい処理は、非同期で処理する。

JavaScriptにおける非同期処理

非同期処理のSampleとして、1番わかりやすいのが、setTimeout() を使用した時間のかかる処理です。

非同期処理として設定されたタスクは、処理を実行する際に、処理の完了を待たずにバックグラウンド処理になる」をCodeで、実感してみます。

console.log("1番目");

// 1秒後に実行する処理
setTimeout(() => {
  console.log("2番目(1秒後に実行される非同期処理)");
}, 1000);

console.log("3番目");

// [ 実行結果 ]
// 1番目
// 3番目
// 2番目(1秒後に実行される非同期処理)

Promiseは、処理の順序の「秩序」(約束・ルール)を作る

Promise を日本語に翻訳すると「約束」です。

処理の順序に「お約束」(ルール・秩序)を取り付けることができるものが Promise だと考えるとわかりやすいです。

Promiseを使用することで、resolve関数 または、reject関数 のどちらかが実行されるまで処理が待機されます。

また、resolve関数reject関数 かのどちらが使用されたかによって、その後に続く処理を分岐することができます。

resolve() で処理が終了(成功)したことを伝えると then() の処理に続きます。

reject() で処理が処理が終了(失敗)したことを伝えると catch() の処理に続きます。

上記のような処理の順序に「お約束」(ルール・秩序)を作ることができるのが、Promiseの特徴です。


console.log("1番目");

let successFlag = true;

// お約束を取り付けたい処理にPromise
new Promise((resolve, reject) => {

  //1秒後に実行する処理
  setTimeout(() => {
    console.log("2番目(1秒後に実行される非同期処理)");

    if (successFlag) {
        // resolve() で処理が終了(成功)したことを伝える => then() の処理に続く・・・
        resolve();
    } else {
        // reject() で処理が終了(失敗)したことを伝える => catch() の処理に続く・・・
        reject();
    }
    
  }, 1000);
})
.then(() => {

  // 処理が無事終わったことを受けとって実行される処理
  console.log("resolve時(成功時)に実行される処理: 3番目");
})
.catch(()=> {

  console.log("reject時(失敗時)に実行される処理: 3番目");
});

上記のSampleCodeをDevToolsのコンソールで実行すると次のような結果が出力されます。

Promiseによって、2番目の処理が完了してから、3番目の処理に移行しているのがわかります。

Promiseの仕組みを理解する

ここからは、Promiseの仕組みなどを細かく見ていきます。

Promiseには3つの状態がある

まず理解しておくべきPromiseの特徴として、3つの状態 (status)があります。

Promise には、PromiseStatus というstatusがあり、

Promise の状態は、常に3つのstatus のいずれかになります。

  1. 待機 (pending): 初期状態。成功も失敗もしていません。
  2. 履行 (fulfilled): 処理が完了したことを意味します。
  3. 拒否 (rejected): 処理が失敗したことを意味します。

new Promise()で作られたPromiseオブジェクトは、pendeingというPromiseStatusで作られます。

処理が成功 (resolve関数の実行)した時に、PromiseStatusfulfilledに変わり,thenに書かれた処理が実行されます。

処理が失敗 (reject関数の実行)した時は、PromiseStatusrejectedに変わり、catchがあれば catchの処理が実行されて、PromiseStatusfulfilled に変わります。

上記の特徴を理解した上で、Promiseの書き方などを見ていきましょう。

Promiseの書き方

Promiseの基本構文は、次のとおりです。

// [ Promiseの書き方 ]
// Promiseインスタンスの作成・構文

const promise = new Promise((resolve, reject) => {
    console.log('何かの処理');
}).then(()=> {
    console.log('成功時の処理 (resolve関数呼び出し後に実行される処理)');
}).catch(()=> {
    console.log('失敗時の処理 (reject関数呼び出し後に実行される処理)');
});

まずはnew Promise() で Promiseのインスタンスを生成して、引数にはコールバック関数を設定します。

また、コールバック関数の第1引数は resolve関数、第2引数は reject関数を設定します。

この resolve関数reject関数の名前は自由に設定できますが、resolvereject にするのが一般的です。

第1引数の resolve関数は、成功時の then に処理を引き継ぎます。

つまり、処理が終わり、無事成功した状態に Promise の PromiseStatus を更新して、then に処理が移行するわけです。

第2引数の reject関数は、失敗時の catch に処理を引き継ぎます。

つまり、処理が失敗に終わってしまった状態に Promiseの PromiseStatus を更新して、catch に処理が移行するわけです。

resolve関数を実行する

まずは、resolve関数の実行結果を見てみましょう。

const promise = new Promise((resolve) => {
    resolve();
}).then(() => {
    console.log("処理成功: resolve");
});

console.log(promise);

resolve関数の引数に値を渡すと、それは then のコールバック関数で引数として受け取れます。

また、最終的に return をすることで、return による実行結果(返り値)が PromiseResult となります。

const promise = new Promise((resolve) => {
    resolve("処理成功: resolve");
}).then((val) => {
    return val;
});

console.log(promise);

reject関数を実行する

次に、reject関数の実行結果を見てみましょう。

reject関数を使用すると、PromiseStaterejected に変わります。

そして、catch節がないとエラーを発生します。

const promise = new Promise((resolve, reject) => {
    reject('処理失敗: reject');
});

console.log(promise);

reject関数を使用すると、PromiseStaterejected に変わりますが、

その後tのcatch節で処理が実行されると PromiseStatefulfilled に変わります。

const promise = new Promise((resolve, reject) => {
    reject('処理失敗: reject');
})
.catch((error) => {
    console.log(error);
});

console.log(promise);

Promiseのメソッドチェーン: then()やcatch()の後に、thenをさらに続ける

resolve (処理成功)した時に then に書かれた処理が実行され、

reject (処理失敗)した時は catch に書かれた処理が実行されますが、

それらの処理の後にまた別の then を実行することができます。

このような Promisethen() catch() の後に、then() をさらに続けて処理をすることを Promiseのメソッドチェーンと言います。

もちろん、通常どおり、return で返した値を第一引数として次の then に渡すことが可能です。

次のSampleは、resolve()then()した後に、 さらにthen() でメソッドチェーンをしています。

// Promiseのメソッドチェーン: then()やcatch()の後に、thenをさらに続けることができる!

// 1. resolveした場合
new Promise((resolve, reject) => {
    resolve("resolve: 処理成功");
})
.then((val) => {
    console.log(`then1: ${val}`);
    return val;
})
.catch((val) => {
    console.log(`catch: ${val}`);
    return val;
})
.then((val) => {
    console.log(`then2: ${val}`);
});

// [ 実行結果 ]
// then1: resolve: 処理成功
// then2: resolve: 処理成功

こちらは、reject()catch()した後に then()でメソッドチェーンしています。


// 2. rejectした場合
new Promise((resolve, reject) => {
    reject("reject: 処理失敗");
})
.then((val) => {
    console.log(`then1: ${val}`);
    return val;
})
.catch((val) => {
    console.log(`catch: ${val}`);
    return val;
})
.then((val) => {
    console.log(`then2: ${val}`);
});

// catch: reject: 処理失敗
// then2: reject: 処理失敗

Promise.all

Promise.all() は配列でPromiseオブジェクトを渡し、すべてのPromiseオブジェクトが resolved になったら次の処理に進みます。

すべての処理が適切に終了したことを確認したうえで、新しい処理を行いたい場合に Promise.all() は使えます。

Promise.all() の引数では、配列の形でPromiseたちの実行結果(返り値)を受け取れます。

// < Promise.all() >

const promise1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve();
  }, 1000);
}).then(() => {
  console.log("promise1おわったよ!");
  return "promise1";
});

const promise2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve();
  }, 3000);
}).then(() => {
  console.log("promise2おわったよ!");
  return "promise2";
});

// 配列でPromise-Objectを渡す! => すべてがresolveになったら、then の処理に移行する!
Promise.all([promise1, promise2])
  .then(([val1, val2]) => {
    console.log(`${val1}&${val2}完了、Promise全部おわったよ!`);
  });

// [ 実行結果 ]
// promise1おわったよ!
// promise2おわったよ!
// promise1&promise2完了、Promise全部おわったよ!

const promiseA = new Promise((resolve, reject) => {
    resolve(123)
  });
  
  const promiseB = new Promise((resolve, reject) => {
    resolve('string')
  });
  
  const promiseC = new Promise((resolve, reject) => {
    resolve(true)
  });
  
  Promise.all([promiseA, promiseB, promiseC]).then((results) => {
    console.log(results);
  });

// < 実行結果 >
// [123, 'string', true]

Promise.all() は、1つでも処理が失敗すると実行されません。

const promiseA = new Promise((resolve, reject) => {
  resolve(123)
});

const promiseB = new Promise((resolve, reject) => {
  resolve('string')
});

const promiseC = new Promise((resolve, reject) => {
  reject(false)
});

// promiseCがrejectされたため、実行されない
Promise.all([promiseA, promiseB, promiseC]).then((results) => {
  console.log(results);
});

// < 実行結果 >
//   [[Prototype]]: Promise
//   [[PromiseState]]: "rejected"
//   [[PromiseResult]]: false
//   Error: Uncaught (in promise) false

Promise.race

Promise.race() は、Promise.all()と同じく配列でPromiseオブジェクトを渡し、どれか1つのPromiseオブジェクトがresolvedになったら次に進みます。

ちなみに、then() の引数には、先に処理が完了したどちらかの実行結果が渡されます。

raceは「競争・追いかけっこ」という意味です。

// < Promise.race >

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 1000);
  }).then(() => {
    console.log("promise1おわったよ!");
    return "promise1";
  });
  
  const promise2 = new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 3000);
  }).then(() => {
    console.log("promise2おわったよ!");
    return "promise2";
  });
  
  // 配列でPromise-Objectを渡す! => どれか1つのPromiseオブジェクトがresolvedになったら次の処理に移行する!
  Promise.race([promise1, promise2])
  .then((val) => {
    // thenの引数には、先に処理が完了したどちらかの実行結果が、渡される。
    console.log(`${val}が先に終わったよ!`);
  });

// [ 実行結果 ]
//   promise1おわったよ!
//   promise1が先に終わったよ!
//   promise2おわったよ!

Promise.finally

finallyメソッドとは、処理の成功・失敗に関わらず、その先の処理を継続して行うメソッドです。

Promiseチェーンのさいごに必ず呼び出したい処理などを定義することができます。

thenメソッドとの違いは、Promiseの処理結果を判断する必要がない点です。

成功したか失敗したかは関係なく、Promiseが確定された段階で処理を行うということになります。

thenメソッドでは、処理が成功した場合と失敗した場合の処理を記述するためには、引数を2つ用意する必要があります。

// [ thenの場合 ]

const promise = new Promise((resolve, reject) => {
    const successFlag = false;
    if (successFlag) {
      resolve('成功');
    } else {
      reject('失敗')
    }
})
.then(
      result => console.log(result),
      error => console.log(error)
);

// [ 実行結果 ]
// 失敗

しかし、finallyメソッドを使えば、結果に関係なく実行したい処理を実行することができます。

// [ finallyの場合 ]

const promise = new Promise((resolve, reject) => {
  const successFlag = false;
  if (successFlag) {
    resolve('成功');
  } else {
    reject('失敗');
  }
})
.then(
  result => console.log(result),
  error => console.log(error)
)
.finally(() => console.log('結果に関係なく処理'));

// [ 実行結果 ]
// 失敗
// 結果に関係なく処理

thenメソッドのエラーハンドリングと catchメソッドは、どちらが優先されるのか?

thenメソッドのエラーハンドリングと catchメソッドでは、thenメソッドのエラーハンドリングの方が優先されます。


const promise = new Promise((resolve, reject) => {
    const successFlag = false;
    if (successFlag) {
        resolve('成功');
    } else {
      reject('失敗');
    }
  })
  .then(
    (result)=>{
        console.log('then', result);
    },
    (error) => {
        console.log('then', error); // こちらが優先される!
    }
  )
  .catch((error)=>{ 
    // catchメソッドは、rejectedステータスのPromiseオブジェクトを受け取ります。
    // 実質的には、thenメソッドでrejectedステータスを扱う場合と同じですが、コードが簡潔化されます。
    console.log('catch', error);
  })
  .finally(() => console.log('結果に関係なく処理'));

  // [ 実行結果 ]
  // then 失敗
  // 結果に関係なく処理

JavaScript書籍 Ver. 中級-上級者向け

JavaScript書籍 Ver. 初級者向け

Twitterやってます!Follow Me!

神聖グンマー帝国の逆襲🔥

神聖グンマー帝国の科学は、世界一ぃぃぃぃぃぃ!!!!!

参考・引用

  1. 難解なマルチスレッド・プログラミングを基礎から解説
  2. 【ES6】 JavaScript初心者でもわかるPromise講座
  3. 【JavaScriptの応用】Promise -finally・Promise.all
  4. Promise.prototype.finally()

最近の投稿