Logo Image

Blog Article

JavaScript Promise 初心者でもわかる非同期処理の基本

00

Promiseとは?

Promise (プロミス) は、非同期処理の結果を表すオブジェクトです。簡単に言うと、「今は結果がわからないけど、あとで成功するか失敗するかを約束するもの」です。

なぜ Promise が必要なのか?

JavaScript では非同期処理 (例えば API のリクエストやファイルの読み込み) が多く使われます。昔は callback という方法で処理していましたが、コードが複雑になりやすいという問題がありました (いわゆる 「コールバック地獄」)

// コールバック地獄の例
getUser(1, function (user) {
  getPosts(user.id, function (posts) {
    getComments(posts[0].id, function (comments) {
      console.log(comments)
    })
  })
})

このようなネスト (入れ子) が深くなる と、可読性が下がってしまいます。これを解決するために Promiseが導入されました。

Promiseの基本構造

const promise = new Promise((resolve, reject) => {
  // 非同期処理を実行
  let success = true

  if (success) {
    resolve('成功!') // 成功時の処理
  } else {
    reject('失敗…') // 失敗時の処理
  }
})

Promise は 3 つの状態を持ちます。

  1. pending (保留) - まだ結果が決まっていない状態。まだ非同期処理が完了していません。
  2. fulfilled (成功) - resolve() が呼ばれて成功した状態。この状態になった時、Promiseは解決され、結果がthen()に渡されます。
  3. rejected (失敗) - reject() が呼ばれて失敗した状態。この状態になった時、Promiseは拒否され、catch()に渡されます。

引数のresolveとrejectは関数なの?

resolverejectは、関数です。new Promise()コンストラクタに渡されるコールバック関数内で、resolverejectはそれぞれ引数として渡されます。これらは、非同期処理が正常に完了したときや、エラーが発生したときに Promise の状態を変更するための関数です。

const promise = new Promise((resolve, reject) => {1
  // ここで `resolve` と `reject` は関数として使われます
});
  • resolve: 非同期処理が成功した場合に呼ばれ、Promise の状態をfulfilled (解決済み)に変更します。resolve() が呼ばれると、Promise は成功として解決され、その値が .then() のコールバックに渡されます。
  • reject: 非同期処理が失敗した場合に呼ばれ、Promise の状態を rejected (拒否) に変更します。reject() が呼ばれると、Promise はエラーとして拒否され、そのエラーメッセージが .catch() のコールバックに渡されます。

Promiseをインスタンス化する理由ってなに?

  1. 非同期処理の結果が成功か失敗かを管理するために、Promiseをインスタンス化します。
  • resolve() : 成功時の値を返す
  • reject() : 失敗時のエラーを返す
const myPromise = new Promise((resolve, reject) => {
  const success = true; // 成功するかどうか(仮の条件)

  setTimeout(() => {
    if (success) {
      resolve("成功しました!");
    } else {
      reject("エラーが発生しました");
    }
  }, 1000);
});

myPromise
  .then((result) => console.log(result)) // 成功時: "成功しました!"
  .catch((error) => console.log(error)); // 失敗時: "エラーが発生しました"
  1. .then().catch()を使って、処理をつなげるため

new Promise()で作成したインスタンス (promiseオブジェクト) には、 .then().catch()などのメソッドがあり、非同期処理を連鎖的に実行できます。

const fetchData = new Promise((resolve) => {
  setTimeout(() => {
    resolve("データを取得しました");
  }, 1000);
});

fetchData
  .then((data) => {
    console.log(data); // 1秒後: "データを取得しました"
    return "次の処理へ";
  })
  .then((nextStep) => {
    console.log(nextStep); // "次の処理へ"
  });
  1. 非同期処理をカスタマイズできる

API のリクエストやファイルの読み込みなど、特定の処理をラップしてPromiseとして扱うことで、再利用しやすくなります。

function asyncTask() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("タスク完了");
    }, 1000);
  });
}

asyncTask().then((message) => console.log(message)); // 1秒後: "タスク完了"

Promiseのインスタンス化とは具体的に何をしているのか?

インスタンス化とは、クラス (設計図) からオブジェクト (実体)を作ることを指します。

JavaScript の Promiseクラス (関数オブジェクト) なので、new Promise()を使うことで Promiseのインスタンス (オブジェクト) を作成しています。そのオブジェクトは 「非同期処理の結果を持つオブジェクト」 になります。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("成功!");
  }, 1000);
});

console.log(promise); // すぐに「Promise { <pending> }」が表示される

変数promiseには、Promiseオブジェクトが格納されています。つまり、Promiseクラスのインスタンス (オブジェクト)を作成しています。

例:オンラインショップでの注文

オンラインショップで商品を注文し、支払いが完了したら商品を発送する流れを Promise で表現してみます。

  1. 商品を注文する (orderProduct())
  2. 注文が完了すると、支払いが行われる (2秒後)
  3. 支払いが完了すると、商品が発送される (shipProduct())
  4. 商品の発送が完了すると、通知が届く (1.5秒後)
function orderProduct(product) {
  return new Promise((resolve, reject) => {
    console.log(`${product} を注文しました。`);
    setTimeout(() => {
      resolve(`${product} の支払いが完了しました。`);
    }, 2000); // 2秒後に支払い完了
  });
}

function shipProduct(paymentConfirmation) {
  return new Promise((resolve) => {
    console.log(paymentConfirmation);
    setTimeout(() => {
      resolve("商品が発送されました。");
    }, 1500); // 1.5秒後に発送
  });
}

orderProduct("ノートPC")
  .then((paymentConfirmation) => {
    return shipProduct(paymentConfirmation);
  })
  .then((shippingStatus) => {
    console.log(shippingStatus);
  })
  .catch((error) => {
    console.log(`エラー: ${error}`);
  });

orderProduct(product)の動作

function orderProduct(product) {
  return new Promise((resolve, reject) => {
    console.log(`${product} を注文しました。`);
    setTimeout(() => {
      resolve(`${product} の支払いが完了しました。`);
    }, 2000); // 2秒後に支払い完了
  });
}
  1. orderProduct() が呼ばれると、新しいPromiseを作成
  2. console.log() で「商品を注文しました」と表示 (この時点では、まだPending)
  3. setTimeout() で2秒後にresolve()を実行

    resolve()が実行されると、支払い完了のメッセージ ("ノートPC の支払いが完了しました。") がPromiseの成功 (fulfilled) の結果として渡される

shipProduct(paymentConfirmation)の動作

function shipProduct(paymentConfirmation) {
  return new Promise((resolve) => {
    console.log(paymentConfirmation);
    setTimeout(() => {
      resolve("商品が発送されました。");
    }, 1500); // 1.5秒後に発送
  });
}
  1. shipProduct()が呼ばれると、新しいPromiseを作成
  2. console.log()で**paymentConfirmation** (支払い完了メッセージ) を表示 (まだpending)
  3. setTimeout()1.5秒後にresolve()を実行

    resolve()が実行されると、「商品が発送されました。」というメッセージが Promise の成功の結果になる

then()を使って処理を順番に実行

orderProduct("ノートPC")
  .then((paymentConfirmation) => {
    return shipProduct(paymentConfirmation);
  })
  .then((shippingStatus) => {
    console.log(shippingStatus);
  })
  .catch((error) => {
    console.log(`エラー: ${error}`);
  });
  1. orderProduct("ノートPC")を呼び出す (すぐに console.log("ノートPC を注文しました。") を実行)
  2. 2秒後に支払い完了resolve("ノートPC の支払いが完了しました。")
  3. .then((paymentConfirmation) => { ... })に渡され、shipProduct(paymentConfirmation)を実行
  4. 1.5秒後に発送完了resolve("商品が発送されました。")
  5. .then((shippingStatus) => console.log(shippingStatus))に渡され、「商品が発送されました。」を表示

なぜ、new Promiseと書かなくても、.then().catch()が実行できるのか

このコードでは、orderProduct関数自体がPromiseを返しているため、.then().catch()を使って、Promiseの結果を処理することができます。

orderProduct関数は、new Promise()を使ってPromiseを生成し、そのPromiseが解決される(resolve() が呼ばれる) ことで結果を返します。この関数自体がPromiseを返すので、次のように.then().catch()を使ってその結果を処理できます。

このように記述すると、より長い処理の連鎖を作成しても、コールバック地獄を防ぐことができます。

Promiseを返さないとどうなる?

Promiseを返さずに非同期処理を実行すると、処理の完了を.then()チェーンで待てなくなります。つまり、非同期処理が終わる前に次の処理が進んでしまいます。

悪い例

function orderProduct(product) {
  return new Promise((resolve) => {
    console.log(`${product} を注文しました。`);
    setTimeout(() => {
      resolve(`${product} の支払いが完了しました。`);
    }, 2000);
  });
}

function shipProduct(paymentConfirmation) {
  console.log(paymentConfirmation); // ここでプロミスを返していない!
  setTimeout(() => {
    console.log("商品が発送されました。");
  }, 1500);
}

orderProduct("ノートPC")
  .then((paymentConfirmation) => {
    shipProduct(paymentConfirmation); // ← ここで return していない
  })
  .then(() => {
    console.log("発送完了後に実行したい処理");
  });

// 実行結果
// ノートPC を注文しました。
// (2秒後) ノートPC の支払いが完了しました。
// (2.5秒後) 発送完了後に実行したい処理 ← ここが先に実行される!
// (3.5秒後) 商品が発送されました。 ← 後から出る

shipProduct()が非同期なのに .then()のチェーンに含まれないので、「発送完了後に実行したい処理」が発送完了前に実行されてしまいます。

.then() の中で非同期処理をするときは、必ずPromiseをreturnしましょう。そうしないと「浮いているプロミス」になり、正しく処理を待てなくなります。

そして、次の.then()は、前の.then()の処理が終わるのを待ってから実行されます。

なので「浮いているプロミス」を防ぐには、.then() の中で return を忘れないようにしましょう。

Promiseを使った非同期処理

例えば、fetchを使ってAPIからデータを取得するコードはPromiseで書かれています。

このコードでは、returnでPromiseを返していないし、resolve()reject() を書いていないのに、.then().catch()を記述しています。

ここで使われているfetch関数は、すでにPromiseを返す関数 なので、new Promise()を書く必要がありません。また、resolve()reject()を使っていないのは、fetchがその内部で既に resolverejectを使っているからです。

fetch('<https://jsonplaceholder.typicode.com/posts/1>')
  .then((response) => response.json()) // レスポンスを JSON に変換
  .then((data) => console.log('記事:', data)) // 取得したデータを表示
  .catch((error) => console.error('エラー:', error)) // エラー時の処理

fetchがPromiseを返す

fetchは標準的なブラウザAPIの一つで、ネットワークリクエスト (例: API へのGETリクエスト) を行います。そして、fetchはPromiseを返す非同期関数です。つまり、fetch自体がPromiseの役割を果たしているため、さらにnew Promise()を使って新しいPromiseを作成する必要はありません。

fetchがPromiseを返した際に、resolverejectが不要な理由

resolve()reject()は、通常は新しいPromiseを手動で作成した場合に使います。しかし、fetch は内部で自動的に次のような動作をしています。

  • 成功した場合、resolve() を呼び出してPromiseを解決 (fulfilled)します。
  • 失敗した場合、reject() を呼び出して Promise を拒否 (rejected) します。

そのため、fetch を使う場合には、resolverejectを自分で書く必要はありません。fetchがネットワークリクエストの成功または失敗に基づいて、Promise を解決 (resolve())または拒否(reject())します。

コメント

ログインしてコメントしましょう。