🍟

Promiseを理解したい、、、!

に公開

今までpending、fulfilled、rejectedの状態を持っている非同期に使えるもの、、、?という理解しかしていませんでしたが、アウトプットがてら書きます。

Promiseとは

非同期処理の最終的な完了もしくは失敗を表すビルドインオブジェクト。
非同期処理はPromiseのインスタンスを返し、そのPromiseインスタンスには状態変化をした際に呼び出されるコールバック関数を登録できる。

Promiseが登場する以前、JavaScriptで非同期処理を行う方法はコールバック関数だった。
時間のかかる処理が終わった後に実行する関数を引数として渡す方法。
このように複数の非同期処理が連続すると、コールバック関数が深くネストして、可読性の低下やエラー処理の複雑化などの問題があった。

setTimeout(() => {
  const groundBeans = "挽いたコーヒー豆";
  console.log("コーヒー豆を挽きました:", groundBeans);
  setTimeout(() => {
    const hotWater = "沸騰したお湯";
    console.log("お湯を沸かしました:", hotWater);
    setTimeout(() => {
      const coffee = "美味しいコーヒー";
      console.log("お湯を使ってコーヒーを淹れました:", coffee);
    }, 3000); // コーヒーを淹れる (3段階目)
  }, 2000);   // お湯を沸かす (2段階目)
}, 1000);     // 豆を挽く (1段階目)

//以下の順番でログが出力される
1秒後 (コーヒー豆を挽きました: 挽いたコーヒー豆)
3秒後 (お湯を沸かしました: 沸騰したお湯)
6秒後 (お湯を使ってコーヒーを淹れました: 美味しいコーヒー)

Promiseの登場

Promiseの登場により、非同期処理の結果をPromiseオブジェクトとして扱うことで、処理の成功、失敗、後続の処理を明確にし連鎖的に記述することが可能になった。

まず、Promiseは以下のいずれの状態になる

  • pending(待機):初期状態
  • fulfilled(履行):処理が完了して成功
  • rejected(拒否):処理が失敗

pending

Promiseが生成された時の初期状態。非同期処理は完了しておらず結果は利用できない。

const myPromise = new Promise(() => {
  setTimeout(() => {}, 1000);
});

//myPromiseの中身はこんな感じ
console.log(myPromise)

Promise
[[Prototype]]:Promise
catch: ƒ catch()
constructor: ƒ Promise()
finally: ƒ finally()
then: ƒ then()
Symbol(Symbol.toStringTag): "Promise"
[[Prototype]]: Object
[[PromiseState]]: "pending"
[[PromiseResult]]: undefined

fulfilled

非同期処理が完了し、Promiseが成功した状態。この状態になると、Promiseは成功時の結果の値を持つ。then()メソッドに登録されたコールバック関数が、この結果の値を受け取って実行。1秒後にログが出力される。

const myPromise = new Promise((resolve) => {
  setTimeout(() => {
    resolve('Hello, world!');
  }, 1000);
});
myPromise.then((result) => {
  console.log('result', result);
});

reject

非同期処理が失敗し、Promiseが失敗した状態。.catch() メソッドに登録されたコールバック関数が、この失敗の原因を受け取って実行される。

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const error = new Error('No hello');
    reject(error);
    }, 1000);
});
myPromise
  .then((result) => {
    console.log('result', result); //実行されない
  })
  .catch((error) => {
    console.log('error', error); //実行される
});

もしくわ、thenメソッドの第一引数にresolve時に呼ばれるコールバック関数、第二引数にreject時に呼ばれるコールバック関数を渡すこともできる

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
   //処理が成功した時に、resolveをコール
   //処理が失敗した時に、rejectをコール
   }, 1000);
});
const onSuccess = () => {
  console.log('success');
};
const onError = () => {
  console.log('error');
};
myPromise.then(onSuccess, onError);

また、プロミスがfulfilled、rejectのいずれかで、pending以外の状態になった場合は、決定(settled) と呼ばれ、settledになるとそれ以降別の状態に変化することはない。

Promiseを使うとどのように書けるのか

冒頭のネストされているコードを Promiseを使うと以下のようになる。
Promiseインスタンスを作成し、resolveで実行。

function delay(ms, value) {
  return new Promise(resolve => setTimeout(() => resolve(value), ms));
}

delay(1000, "挽いたコーヒー豆")
  .then(groundBeans => {
    console.log("コーヒー豆を挽きました:", groundBeans);
    return delay(2000, "沸騰したお湯");
  })
  .then(hotWater => {
    console.log("お湯を沸かしました:", hotWater);
    return delay(3000, "美味しいコーヒー");
  })
  .then(coffee => {
    console.log("お湯を使ってコーヒーを淹れました:", coffee);
  })
  .catch(error => {
    console.error("エラーが発生しました:", error);
  });

async/awaitの登場

Promiseが誕生したことにより、可読性が上がったが、then()メソッドの連鎖が深くなると、コードががやや複雑になる。ここで導入されたのがasync/await。async/await構文は、Promiseを使った非同期処理を同期的なコードのように記述できるようにするシンタックスシュガー(糖衣構文)。

async 関数

async キーワードを関数の宣言の前に追加することで、その関数は暗黙的に Promise を返すようになる。async 関数内では、await キーワードを使用することができる。
上記のコードを書き直すと、以下のようになる。

const processCoffee = async () => {
  console.log("コーヒーを淹れ始めます...");
  try {
    const groundBeansPromise = delay(1000, "挽いたコーヒー豆");
    const groundBeans = await groundBeansPromise;
    console.log("コーヒー豆を挽きました:", groundBeans);

    const hotWaterPromise = delay(2000, "沸騰したお湯");
    const hotWater = await hotWaterPromise;
    console.log("お湯を沸かしました:", hotWater);

    const coffeePromise = delay(3000, "美味しいコーヒー");
    const coffee = await coffeePromise;
    console.log("お湯を使ってコーヒーを淹れました:", coffee);
    console.log("☕ ができました!:", coffee);

  } catch (error) {
    console.error("エラーが発生しました:", error);
  }
};

processCoffee();

await キーワード

await キーワードは、async 関数内でのみ使用でき、Promise の前に置かれる。await は、その Promise が settled (fulfilled or rejected) になるまで、async 関数の実行を一時停止する。

Promise が fulfilled になると、await 式はその Promise の結果の値を返し、Promise が rejected になると、await 式は例外をスローする。この例外は、try...catch ブロックで捕捉することができる。

最後に

fetchする時に、async/awaitを無意識の内に使っていたが、関係性を調べる事で理解度が深まった気がする。
次はHTTPなど書いてみようかなー。

参考資料

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises
https://jsprimer.net/basic/async/

Discussion