Multi Vitamin & Mineral

Multi Vitamin & Mineral です。プログラムに関することを書いております。

JavaScript の Promise をサクッと知っておく

はじめに

時間が掛かる処理が終わった後、とある処理を動かしたい! という場面で活躍するのが Promise です。ここではその作り方、使い方について書きます。

ここではその作り方、使い方の初歩の初歩について書きます。これを読んでおくことで、後は詳しい書籍やサイトも読みやすくなるよ、という記事になることを目指して書いています。

Promise の目的

ざっくり言うと「非同期処理の仕組み」です。

プログラムは通常上から順に動きます。A, B, C, 3つの処理を書いたら、Aが終わったらB、Bが終わったらCの処理が動き出します。(一つひとつ終わってから次の処理に進むのが「同期」処理です。)

[A始---A終] -> [B始---B終] -> [C始---C終]

ですが、Bは時間がかかる処理なので、Bが終わる前にCを始めちゃいたいよね、というシチュエーションもあると思います。

[A始---A終] -┬-> [B始---B終]
             └---> [C始---C終]

このように、「あるタスクが実行をしている間に他のタスクが別の処理を実行できる」ことを「非同期」といいます。

この「非同期」処理を可能にする仕組みが Promise です。

Promise の基本構造

Promise は以下のように記述して使います。

// 形式1
new Promise(関数1).then(関数2);

エラー系の処理を行わせたい場合は以下のように記述します。

// 形式2
new Promise(関数1).then(関数2).catch(関数3);

この記事では、まずは上記の「形式1」と「形式2」について話をします。

Promise の基本的な使い方(形式1)

以下の「形式1」の使い方を説明します。

// 形式1
new Promise(関数1).then(関数2);

関数1 : 時間が掛かる処理

関数1には時間が掛かりそうな処理を書きましょう。 その関数1は resolvereject という2つの引数を持つ関数になります。(名前は任意でよいが、よくこの名前で書くことが多い。)

// 関数1
(resolve, reject) =>{
  // ...時間の掛かる処理...
  resolve('hoge');
};

関数1の resolvereject は、実は関数です。時間が掛かる処理の後に resolve を呼び出しています。( reject は「形式2」で使います。後述。) resolve には引数をセットすることができます。(ここで 'hoge' としている部分です。)

これを「形式1」に適用すると下記のようになります。

new Promise((resolve, reject) =>{
  // ...時間の掛かる処理...
  resolve('hoge');
}).then(関数2);

関数2 : 関数1の resolve が呼び出す処理

関数2は1つの引数を持つ関数になります。ここでは引数を value としています。

// 関数2
(value) => {
  console.log(value);
};

これも「形式1」に適用します。下記のようになります。 resolve(値) が呼び出されると then(関数2) の中の関数2が呼び出さる仕組みになっています。

new Promise((resolve, reject) =>{
  // ...時間の掛かる処理...
  resolve('hoge');
}).then((value) => { // 'hoge'(resolveの引数)が入ってくる
  console.log(value);
});

回りくどいですが resolve の引数は関数2の引数になります。よって上記の value の中身は 'hoge' になります。

実行結果

先の例を実行してみます。最後にログを追記しておきます。

new Promise((resolve, reject) =>{
  // ...時間の掛かる処理...
  resolve('hoge');
}).then((value) => {
  console.log(value);
});

console.log('fuga'); // ←追加した

結果は以下です。

fuga
hoge

あとから hoge が出力されます。( ...時間の掛かる処理... が数秒時間が掛かった前提くらいに思って読んでください。 )

これで、時間のかかる処理(関数1)を先に実行しておいて、別の処理( console.log('fuga'); )を先に動かしておくことができました。 hoge と fuga の出力処理がいわゆる非同期になった訳です。

エラーが発生した場合の処理を追加する(形式2)

続いて、エラー系の処理を行わせたい場合の「形式2」のパターンです。

// 形式2
new Promise(関数1).then(関数2).catch(関数3);

関数3(その1) : 関数1の reject が呼び出す処理

関数1の2つ目の引数に reject というものがありました。こちらはエラーが起きたときに使います。 reject が呼び出されると catch(関数3) が呼出されます。

new Promise((resolve, reject) =>{
  // ...時間の掛かる処理...
  if (error) {
    reject(new Error('fuga')); // エラーが起きたときに使う。Errorオブジェクトを使うのが筋。
  } else {
    resolve('hoge');
  }
}).then((value) => {
  console.log(value)
}).catch((error) => {
  console.error(error)); // Errorオブジェクトを受け取り出力
});

関数1の中でエラーが発生する可能性があれば、 reject を呼び出し、これに連動する catch(関数3) を用意するようにします。

関数3(その2) : 例外が発生しても呼び出される

関数3は reject が呼び出された場合に実行されますが、もうひとつ別の方法でも呼び出されます。それはJavaScriptの例外です。

以下は reject を例外の throw に置き換えたものです。先程との差分の部分は を付けてコメントしていますので見比べてください。

new Promise((resolve) =>{ // ★:rejectは使わないので引数から外した
  // ...時間の掛かる処理...
  if (error) {
    throw new Error('fuga'); // ★:rejectは使わず throw する
  } else {
    resolve('hoge');
  }
}).then((value) => {
  console.log(value)
}).catch((error) => {
  console.error(error)); // Errorオブジェクトを受け取り出力
});

基本構造についてのまとめ

形式1 と 形式2 について説明をしましたので、ここで内容を簡単にまとめておきます。

new Promise(関数1).then(関数2).catch(関数3);
  • 関数1に時間が掛かる処理を書く
    • 関数1の引数は resolvereject の2つ
    • 処理が成功したら resolve を呼び出す
      • resolve の引数を関数2が受け取る
    • 処理が失敗したら Error を throw する(or reject を呼び出す)
      • throw した値(or reject の引数)を関数3が受け取る
  • 関数2に成功したあとの処理を書く
    • resolve の引数が関数2の引数に渡ってくる
  • 関数3に失敗したあとの処理を書く
    • throw した値(or reject の引数)が関数3の引数に渡ってくる

then はつなげることができる

以上で基本的な使い方はマスターできると思います。以降は応用編として then(...) をつなげて書くパターンを説明します。

分割して記述する

分割といってもたいした話ではないんですが、それを先にしておきます。

new Promise(...) の部分と .then(...) はつなげて書くよう説明しましたが、実際には別の関数にして書くことが多いと思います。

new Promise((resolve, reject) =>{
  // ...時間の掛かる処理...
  resolve('hoge');
}).then((value) => {
  console.log(value);
});

new Promise(...) で生成されたオブジェクトに対して .then(...) をしていますので、以下のように分割しても同じになります。

// (1) runFunc 関数は Promise オブジェクトを返却する
const runFunc = function() {
  // Promise オブジェクトを返却する
  return new Promise((resolve, reject) =>{
    // ...時間の掛かる処理...
    resolve('hoge');
  })
};
// (2) 生成されたオブジェクトの then メソッドを使う
runFunc() // これは Promise オブジェクトになる
.then((value) => {
  console.log(value);
});

runFunc という「Promise オブジェクトを返却する関数」を用意して分割しました。 .then の手前で分割しただけでやっていることは同じです。

分割した関数に引数を渡すこともできます。引数としてホームページのURLを渡したら、タイトル文字列を取得する処理をイメージしています。

// (1) 
const runFunc = function(url) { // ★:url という引数を受ける
  return new Promise((resolve, reject) =>{
    const title = getTitle(url); // ★:getTitle() はホームページのタイトルを取得する関数と思って読んでください
    resolve(title); // ★:取得したタイトルを渡す
  })
};
// (2) 
runFunc('http://xxx.xxx.xxx') // ★:urlを(1)runFuncに渡している
.then((value) => {
  console.log(value); // ホームページのタイトルが出力される
});

こうすると runFunc の処理は汎用的に色んな箇所で利用できることになります。

今回は runFunc を作りましたが、予め用意されている「Promise オブジェクトを返却する関数」を使う場面の方が多いかと思います。ですので、上記例の (1) はひとつだけ作っておき、(2) を書く方が頻出なのだろうな、くらいに思っておくと良いでしょう。

.then(...) のチェーンと .finally(...)

.then(...) は複数つなげて書くことができます。今までに説明した .catch(...) に加え、 .finally(...) という新しい機能も含めてこのことを説明していきます。

runFunc().then((value) => { // 1つめのthen
  // ...処理1...
}).then((value) => {        // 2つめのthen
  // ...処理2...
}).then((value) => {        // 3つめのthen
  // ...処理3...
}).catch((error) => {       // catch
  // ...処理Error...
}).finally(() => {          // finally
  // ...処理Final...
});

このように then はいくつでもつなげることができます。そして最後に finally も付け加えてみました。

さてどのような挙動になるのでしょうか。

例外が throw されなかった場合(もしくは reject が呼ばれなかった場合)は、処理1処理2処理3処理Finalの順に動きます。 then は順番に動く仕様になっています。

もし、処理2の途中で例外が throw されたなら、処理1処理2(途中まで)処理Error処理Finalの順に動きます。

つまり以下のような仕様になります。

  • then は例外が発生しない限りは順番に動く
  • どこかで例外が発生した時点で中断され catch が動く
  • finally は例外の発生に関係なく必ず最後に動く

時間が掛かりそうな通信処理が複数あり、エラーになったら必ず行う処理(「エラーが発生しました!」と通知を出す等)がある場面などなど、この使い方が有用になる場面は多々あるんじゃないでしょうか。

.finally(...) の中の関数は引数は設定できない

then には resolve(...) の引数を受け渡せます。 catch には reject(...) の引数、もしくは投げた例外を受け渡せます。

ですが、 finally はこれらと異なり受け渡せるものはありません。

thencatch と違って finally は起動する契機を自分で用意することはできません。契機なんて用意できないのだから、必然的に渡せるものは用意できませんし、そんなものは必要とされないんだろうな、くらいに思っておけばよいでしょう。

.then(...) のチェーン間で値を渡す

複数の then の間では値を渡すことができます。

const runFunc = function() {
  return new Promise((resolve) => {
    // ...時間の掛かる処理...
    resolve(3);             // 3 を渡す
  })
};
runFunc().then((value) => { // 3 が渡ってくる
  return value + 2;         // 3+2 = 5 を返却する  ★
}).then((value) => {        // 5 が渡ってくる
  return value * 6;         // 5*6 = 30 を返却する ★
}).then((value) => {        // 30 が渡ってくる
  console.log(value);       // 30 が出力される
});

then の引数の関数内で return した値が次の then に渡ります。

おまけ1 : .then(...) は Promise オブジェクトを返している

ここからは、なぜ .thencatchfinally をつなげることができるのか? そして、なぜ return で値を受け渡すことができるのか? という話をします。Promise を利用するだけなら必須の知識ではありませんが、知っておくと理解が深まると思います。

まずは前者の話。

new Promise(...).then(...) と書くことができました。これは Promise クラスに then メソッドがあるからです。Promise クラスから作られた Promise オブジェクトである new Promise(...) は、then メソッドを . でつないで呼び出せます。(これは JavaScript のクラスやオブジェクトの話です。ここではクラスの説明まではしません。)

Promise クラスには catch メソッドも finally メソッドもあります。ですので new Promise(...).catch(...)new Promise(...).finally(...) も当然OKです。

さて、なんで .then(...).catch(...).finally(...) のように、メソッドが . で繋げられるのでしょう?

これは、これらの3メソッドが Promise オブジェクトを返却しているからです。

以下は今まで通りの書き方です。

const runFunc = function() {
  return new Promise((resolve, reject) =>{
    // ...略...
  })
};
runFunc().then((value) => {
  // ...略...
}).then((value) => {
  // ...略...
}).catch((error) => {
  // ...略...
}).finally(() => {
  // ...略...
});

連続している処理を分割してみましょう。Promise オブジェクトを定数で受けてみると以下のようになります。

const runFunc = function() {
  return new Promise((resolve, reject) =>{
    // ...略...
  })
};
const promiseObject1 = runFunc();
const promiseObject2 = promiseObject1.then((value) => {
  // ...略...
});
const promiseObject3 = promiseObject2.then((value) => {
  // ...略...
});
const promiseObject4 = promiseObject3.catch((error) => {
  // ...略...
});
const promiseObject5 = promiseObject4.finally(() => {
  // ...略...
});

新しい Promise オブジェクトを作っては then 、作っては then 、作っては catch 、のようになっていたという訳です。いちいち定数で受けずに書けば、あたかも . でつながっているように見えるという訳です。

こういったメソッドの連結(メソッドチェーン)は jQuery なんかでも頻出でした。仕組みとしては同じですね。

おまけ2 : なぜ return で受け渡せるのか?

なぜ return した値が次に渡されるのか? という話に続きます。

then は内部で新しい Promise オブジェクトを返却していることは説明しました。どのような Promise オブジェクトになっているのか、先程の例の一部分を取り出してみます。

const thenPromiseObject = promiseObject.then((value) => {
  return value * 6;
});

ここで生成されている thenPromiseObject は、内部では以下のようなものになってます。

new Promise((resolve) => resolve(30));

.then(関数A) は、関数Aが return した値を、 resolve(引数B) の引数Bにセットした Promise オブジェクトを生成する低みになっているのです。これにより return しておけば上手いこと次の then に引き継がれる仕組みとなっていたのです。

then をつなげることについてのまとめ

  • new Promise(...) を返却する関数を用意しておくと汎用性が高まる
  • then は連続して . でつなげることができる
  • 途中で例外が発生したら処理は中断され catch の処理に入る
  • 例外に関わらず行う処理は finally に書くことができる
  • then の中の関数で return した値は次の then に受け渡せる

参考

最後に参考になるサイトを掲載しておきます。

techracho.bpsinc.jp

こちらのサイトでは、文字通りざっくりと簡単に説明がされています。当該記事があれば見なくても大丈夫な知識であるとは思います。が、説明の切り口が違いますので、人によっては理解がしやすくなるかも知れません。

azu.github.io

こちらはもっと詳細に丁寧に説明がされたサイトです。仕様の理解をより深め、さらなる知識を得るために役に立つものと思います。