Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
83
Help us understand the problem. What is going on with this article?

More than 1 year has passed since last update.

@cheez921

【ES6】 JavaScript初心者でもわかるPromise講座

はじめに

Promiseって...難しくないですか???
3ヶ月くらい前の私は、Promiseをほとんど理解できてないのにasync/awaitとか使っちゃってたし、様々な記事を読み漁ってもなかなか理解できず、Promiseの正体を掴むのに時間がかかってしまいました。

そんな3ヶ月くらい前の自分にも伝わるように、できる限り丁寧にPromiseを説明していこうと思います。

前提

本記事では、Promise以外のES6の書き方に関しては触れておりません。
アロー関数やテンプレート文字列などを例文で用いているため、わからない方がいましたら下記記事などを参考にしていただけると幸いです。

ES2015(ES6) 入門

Promiseとは

JavaScriptは処理を待てない!

まずはじめに、下記コードを実行してみると、どのような結果になるでしょうか。

console.log("1番目");

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

console.log("3番目");

1番目2番目3番目...という順番で実行されることが理想的ではありますね。

しかし、実際には下記のような順序で実行されてしまいます。

1番目
3番目
2番目(1秒後に実行)

JavaScriptは非同期言語であるため、一つ前の実行に時間がかかった場合、実行完了をまたずに次の処理が行われてしまいます

Promiseは、処理の順序に「お約束」を。

Promiseを日本語に翻訳すると「約束」です。
なので、私はPromiseのことを、処理の順序に「お約束」を取り付けることができるもの、処理を待機することや、その結果に応じて次の処理をすることお約束するものだと思っています。

先ほどの例で詳しく見ていきましょう。
(コードの中身は、後ほど説明していきますので、わからなくて大丈夫です!)

console.log("1番目");

// お約束を取り付けたい処理にPromise
new Promise((resolve) => {
  //1秒後に実行する処理
  setTimeout(() => {
    console.log("2番目(1秒後に実行)");
    //無事処理が終わったことを伝える
    resolve();
  }, 1000);
}).then(() => {
  // 処理が無事終わったことを受けとって実行される処理
  console.log("3番目");
});

すると、実行結果は下記のようになります。

1番目
2番目(1秒後に実行)
3番目

Promiseを用いることで、理想の処理を実行することができました。

それでは、より詳しくPromiseをみていきましょう。

Promiseには3つの状態がある

Promiseには、PromiseStatusというstatusがあり、3つのstatusがあります。

  • pending: 未解決 (処理が終わるのを待っている状態)
  • resolved: 解決済み (処理が終わり、無事成功した状態)
  • rejected: 拒否 (処理が失敗に終わってしまった状態)

new Promise()で作られたPromiseオブジェクトは、pendeingというPromiseStatusで作られます。
処理が成功した時に、PromiseStatusresolvedに変わり,thenに書かれた処理が実行されます。
処理が失敗した時は、PromiseStatusrejectedに変わり、catchに書かれた処理が実行されます。

Promiseの大まかな処理の流れはわかりましたでしょうか。
それでは実際に書いてみましょう...!

Promiseの書き方

Promiseインスタンスの作成

sample1.js
const promise = new Promise((resolve, reject) => {});

Promiseの引数には関数を渡し、その関数の第一引数にresolveを設定し、第二引数にrejectを任意で設定します。
(resolverejectも関数です。)

ちなみに、上記コードにて定義したpromiseconsole.logすると下記のようなPromiseオブジェクトが返ってきます。

Promiseオブジェクト pending

作りたてのPromiseオブジェクトなので、PromiseStatusは、pendeingですね。

resolveさせよう

sample1のコードをresolveさせてみましょう。

// rejectは今回使わないため、引数から削除
const promise = new Promise((resolve) => {
  resolve();
}).then(() => {
  console.log("resolveしたよ");
});

実行結果は下記のようになります。

resolveしたよ

Promiseオブジェクトを確認してみると...
Promiseオブジェクト resolved

resolve()を呼び出すことによって、
PromiseStatusresolvedに変わり,thenの処理が走ってることがわかりますね。

resolve関数に引数を渡してあげると...

resolve関数は、引数を受け取ることができ、次に呼ばれるメソッド(then)の第一引数に渡してあげることができます。

const promise = new Promise((resolve) => {
  // 引数に文字列を渡す
  resolve("resolveしたよ");
}).then((val) => {
  // 第一引数にて、resolve関数で渡した文字列を受け取ることができる
  console.log(val);
});
実行結果.
resolveしたよ

resolve関数の引数にて渡された値が、thenの第一引数に渡されてることがわかりますね。

下記コード時点でのpromiseを確認してみましょう。

const promise = new Promise((resolve) => {
  // 引数に文字列を渡す
  resolve("resolveしたよ");
});

スクリーンショット 2020-03-22 18.24.11.png

PromiseValueに、resolveに渡した文字列が入ってることがわかりましたね。
PromiseValueに格納された値を、次に呼ばれるメソッドの第一引数に渡すことができる、という仕組みになっております。

rejectさせよう

sample1のコードをrejectさせてみましょう。

const promise = new Promise((resolve, reject) => {
  reject();
})
  .then(() => {
    console.log("resolveしたよ");
  })
  .catch(() => {
    console.log("rejectしたよ");
  });

実行結果は下記のようになります。

rejectしたよ

thenの処理は実行されず、catchの処理が実行されることがわかりますね。

Promiseオブジェクトを確認してみると...

スクリーンショット 2020-03-22 18.42.37.png

想定通り、PromiseStatusrejectedに.....なってない!!!!!

なぜrejectedにならない...?

下記コード、つまりcatchの処理を行なっていない状態のPromiseオブジェクトをみてみましょう。

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

Promiseオブジェクト reject

この時点では、PromiseStatusrejectedですね。
ちょっと安心ですね。笑

(Uncaught (in promise)というエラーはrejectされた時に、一回もcatchの処理が行われなかった時にでます。)

実は、catchにて実行される関数がreturnした値をresolveします。
めちゃくちゃ簡単にいうと、catchはエラー返したら満足して、解決済みだ!ってするみたいです。
つまり、catchにて返されたpromiseオブジェクトPromiseStatusresolveになります。

んー、難しい。

Promiseのメソッドチェーン

処理が成功した時にthenに書かれた処理が実行され、処理が失敗した時はcatchに書かれた処理が実行されると説明しましたが、それらの処理の後にまた別のthenを実行することができます。

さらに、returnで返した値を第一引数として次のthenに渡すことが可能です。

resolveした場合.js
const promise = new Promise((resolve, reject) => {
  resolve("test");
})
  .then((val) => {
    console.log(`then1: ${val}`);
    return val;
  })
  .catch((val) => {
    console.log(`catch: ${val}`);
    return val;
  })
  .then((val) => {
    console.log(`then2: ${val}`);
  });
実行結果.
then1: test
then2: test

resolveされたため、1つめのthenが実行されたあと、
returnvalを受け取り、2つめのthenが実行されました。

rejectした場合.js
const promise = new Promise((resolve, reject) => {
  reject("test");
})
  .then((val) => {
    console.log(`then1: ${val}`);
    return val;
  })
  .catch((val) => {
    console.log(`catch: ${val}`);
    return val;
  })
  .then((val) => {
    console.log(`then2: ${val}`);
  });
実行結果.
catch: test
then2: test

rejectされたため、1つめのthenは処理されず、
1つめのcatchが実行されたあとに2つめのthenが実行されました。

Promise.allPromise.race

これらは、Promiseを複数実行して、その結果に応じて次の処理に進む便利メソッドです。

Promise.all

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

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

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

Promise.all([promise1, promise2]).then(() => {
  console.log("全部おわったよ!");
});
実行結果.
promise1おわったよ!
promise2おわったよ!
全部おわったよ!

Promise.race

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

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

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

Promise.race([promise1, promise2]).then(() => {
  console.log("どれか一つおわったよ!");
});
実行結果.
promise1おわったよ!
どれか一つおわったよ!
promise2おわったよ!

async/awaitを使ってもっと簡潔に!

Promiseの処理は、Promiseインスタンスを生成したり,resolve/rejectしたりするため、場合によっては結構冗長的になってしまいます。
async/awaitを使うとPromiseの処理をより簡潔に書くことができます。

例に、下記のPromiseの処理をasync/awaitを使って、リファクタします。

const alwaysLateBoy = (ms) => {
  new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  }).then(() => {
    console.log(`すまん!${ms}ms待たせたな。`);
  });
};

処理としては、引数で渡したms秒分待機して、その後console.logを走らせれば良さそうですね。

上記の処理をasync/awaitを用いると、下記のようにまとめることができます。

// async function を定義
const alwaysLateBoy = async (ms) => {
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  })
  // Promiseの結果が返ってくるまで実行されない
  console.log(`すまん!${ms}ms待たせたな。`)
};

asyncとは

asyncは非同期関数を定義する関数宣言であり、関数の頭につけることで、
Promiseオブジェクトを返す関数にすることができます。
そのような関数をasync functionといいます。

const asyncFunc = async () => {
  return 1;
};

上記async functionを実行すると、下記のようなPromiseオブジェクトが返ってきます。

スクリーンショット 2020-03-22 22.00.44.png

async functionが値をreturnした場合、Promiseは戻り値をresolveし、
その値はPromiseValueとして扱われます。

awaitとは

awaitは、Promiseオブジェクトが値を返すのを待つ演算子です。
await必ず、async function内で使います

下記のコードを例に考えてみましょう。

const asyncFunc = async () => {
  let x, y;
  x = new Promise(resolve => {
    setTimeout(() => {
      resolve(1);
    }, 1000 )
  })
  y = new Promise(resolve => {
    setTimeout(() => {
      resolve(1);
    }, 1000 )
  })
  console.log(x + y)
}

この処理では、Promiseがresolveするのを待つ処理を書いてないため、
console.log(x + y)では、下記のような結果が返ってきます。

実行結果...
NaN

この処理を、Promiseがresolveするのを待つようにするには、
このPromiseオブジェクトが全て実行完了するのを待つPromise.allを使えばできそうですが、awaitを使うともっと簡単に実装できます。

const asyncFunc = async () => {
  let x, y;
  // Promiseがresolveするまで待機
  x = await new Promise((resolve) => {
    setTimeout(() => {
      resolve(1);
    }, 1000);
  });
  // Promiseがresolveするまで待機
  y = await new Promise((resolve) => {
    setTimeout(() => {
      resolve(1);
    }, 1000);
  });

  console.log(x + y);
};

console.log(x + y)は、上二つのPromiseが値を返すのを待ってから実行されるため、実行結果は下記のようになります。

実行結果...
2

おわりに

想像以上に長くなってしまい、後半からわかりづらいかもしれません。。。泣
少しでもPromiseに対して苦手意識がなくなったら良いなと思います。
(自分自身、この記事を書いていくうちに、さらにPromiseを理解できたような気がします。)

参考記事

83
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
83
Help us understand the problem. What is going on with this article?