LoginSignup
6
6

More than 3 years have passed since last update.

3日で忘れるPromiseまとめ

Last updated at Posted at 2020-05-25

概要

  • Promise、身近な割に挙動詳細をわすれやすいので、いろんなパターンをコードを実際に書いて実行してみて思い出すための呼び水メモ、です

Promiseのポイント

Promiseには3つの「状態」がある

  • pending 初期状態
  • fulfilled 成功状態
  • rejected 失敗状態

Promiseのコンストラクタにはexecutor関数をセットし、Promiseに「値」をセットさせる

  • executor関数とは↓なやつ
 (resolve,reject)=>{} 

つまり↓のようにしてPromiseオブジェクトを作る

new Promise((resolve,reject)=>{});
  • executor関数には 一般的に、処理がおわるまでに待ちが発生するような、非同期に実行したい処理を書く
  • このexecutor関数内で resolve(値) を呼び出すと、Promiseはpending状態からfulfilled状態になる。
  • 逆にreject(値)を呼び出すと、Promiseはpending状態からrejected状態になる。

Promiseは「値」を持つ

  • Promiseはfulfilled,rejectedの状態になるときに「値」ももつ(値はundefinedも可)
  • Promiseの「値」はresolve(値)、reject(値)の呼び出しによってPromiseにセットされる。

(セットされた「値」はthenメソッドのコールバック関数の引数に入って、次の処理に受け渡される。→後述する)

つまり、Promiseは「状態」と「値」を持つ とおぼえる

ということで、
Promiseには 状態(pending→fulfilled or rejected) と 値(非同期処理終了後にexecutor関数でセットされた値) を持つ とおぼえておく。

image.png

Promiseにはthenメソッドがある

  • Promise内(executor関数)の処理が終わると、Promiseは、その状態が「fulfilled」か「rejected」に状態が変化した後、Promiseのthenメソッドに指定した関数が呼ばれる
  • thenには2つの引数をとる。1つでもいい。
  • 2つの引数にはPromiseがfulfilled状態になったとき(成功したとき)と、rejected状態になったとき(失敗したとき)のコールバック関数 を指定する
.then(
//成功したときのコールバック関数
(value)=>{},
//失敗したときのコールバック関数
(value)=>{}
) 
  • Promise内で resolve

  • thenメソッドは必ずPromiseを返却する

  • thenメソッドの引数に指定したコールバック関数内で何をreturnしようと、かならずthenメソッドの返値はPromiseになる。これ重要。

  • thenメソッドは必ずPromiseが返却されるので、then().then() とチェインできる。thenはPromiseのメソッドなので。

実際に動かしておもいだす

1.Promise.resolve()でfulfilled状態のPromiseを扱う

Promise.resolve("value1")を呼び出すと直接 fulfilled状態のPromiseが返値となる

すでに Promise.resolve("value1") はfullfilled状態なので、thenメソッドをつなげれば、すぐにthenメソッドに指定したコールバック関数に処理がうつる

See the Pen Promiseサンプル1 by Tom Misawa (@riversun) on CodePen.

2.Promiseは遅延実行じゃない、作ったらすぐ実行される

Promiseが生成されたら、(executor関数の)中に書いた処理はすぐ実行される

  // Promiseのコンストラクタには引数を2つ(resolve,reject)とる関数をわたす。
  const p2 = new Promise((resolve, reject) => {
    console.log("Promise(p2)のexecutor関数の中で2秒かかる非同期な処理中");
    setTimeout(function () {
      // 非同期処理をして、おわったらresolve
      console.log("非同期な処理終了!");
      //非同期処理が成功ならresolve,失敗ならrejectを呼び出す。
      //引数には、実行結果を入れる。これがthenで返却されるPromiseの値となる
      resolve("value2"); // resolveして成功とする
    }, 2000);
  });

See the Pen Promiseサンプル2 by Tom Misawa (@riversun) on CodePen.

3.PromiseはPromise外の処理をブロックしない

ブロックしちゃったら、Promiseの存在価値なし

  const p3 = new Promise((resolve, reject) => {
    console.log("Promise(p3)の中で非同期処理中。");
    setTimeout(function () {
      console.log("Promise(p3)の非同期処理終了");
      resolve("value3");
    }, 1000);
  });

  console.log("PromiseはPromise外のコード実行をブロックしない");

  p3.then((value) => {
    console.log(`Promise(p3)の実行結果:${value}`);
  });

See the Pen Promiseサンプル3 by Tom Misawa (@riversun) on CodePen.

4.thenメソッドは2つのコールバック関数を指定できる

Promiseがfulfilledかrejectedの状態になったとき、thenメソッドに指定されたコールバック関数が実行される

thenメソッドは、then(成功したときのコールバック関数,失敗したときのコールバック関数)のように記述する。

4-0.fulfilledなPromiseは、thenメソッドに指定した成功したときのコールバック関数が呼ばれる

// p40は値value40を持つfulfilledな状態のPromiseとなる
const p40 = Promise.resolve('value40');
p40.then(
  (value) => {
    console.log(`Promise(p40)はresolveされました 値:${value}`);
  },
  (value) => {
    console.log(`Promise(p40)はrejectされました 値:${value}`);
  });

See the Pen Promiseサンプル40 by Tom Misawa (@riversun) on CodePen.

4-1. rejectedなPromiseは、thenメソッドに指定した第二引数の失敗したときのコールバック関数が呼ばれる

// p41は値value41を持つfulfilledな状態のPromiseとなる
const p41 = Promise.reject('value41');
p41.then(
  (value) => {
    console.log(`Promise(p41)はresolveされました 値:${value}`);
  },
  (value) => {
    console.log(`Promise(p41)はrejectされました 値:${value}`);
  });

See the Pen Promiseサンプル41 by Tom Misawa (@riversun) on CodePen.

4-2. thenメソッドの引数となるコールバック関数を省略すると、thenメソッドは最後に実行されたPromiseの状態を受け継いだPromiseを生成して返す

const p42 = Promise.reject('value42');
p42
  // p42はrejected状態だけど、このthenメソッドにはrejected処理用のコールバック関数の指定が無い
  // rejected処理用のコールバック関数がないので、このthenメソッドはp42と同じ状態のPromiseを返す
  .then((value) => {
    console.log(`Promise(p42)はresolveされました 値:${value}`);
  })
  // ↓チェインされたこちらのthenメソッドに指定されてるrejected用のコールバック関数が呼ばれる
  .then(null, (value) => {
    console.log(`Promise(p42)はrejectされました 値:${value}`);
  });

最初に登場するthenメソッドにはreject用のコールバック関数がない。その場合は、thenメソッドは最後のPromiseの状態を受け継いでいるPromiseを生成する。この例でいえば、thenメソッドは「値として"value42"もち、rejected状態」の新たなPromiseを生成する。
この仕組みにより、ハンドリングされなかったPromiseがチェインされたthenメソッドで後段に受け継がれていくようなコードを書くことができる。

See the Pen Promiseサンプル42 by Tom Misawa (@riversun) on CodePen.

4-3 catchメソッドはthen(null,(value)=>{})の短縮版

上でもみたようにrejected状態のPromiseを処理する関数はthen(null,(value)=>{//rejectを処理する})のようにthenメソッドの第2引数に指定するが、rejectedなPromiseだけを処理したい場合はthen(null,(value)=>{})は冗長なので、catchメソッドをつかう。

const p43 = Promise.reject('value43');
p43
  .then((value) => {
      console.log(`Promise(p43)はresolveされました 値:${value}`);
    }
  )
  .catch((value) => {
    console.log(`Promise(p43)はrejectされました 値:${value}`);
  });

See the Pen Promiseサンプル43 by Tom Misawa (@riversun) on CodePen.

ということで、catchメソッドは引数1つ。引数にはPromiseが失敗したときのコールバック関数、つまりrejectedなPromiseを処理するための関数だけ指定する。


const p43part2 = Promise.reject('value43part2');
p43part2.catch((value) => {
  console.log(`Promise(p43part2)はrejectされました 値:${value}`);
});

See the Pen Promiseサンプル43part2 by Tom Misawa (@riversun) on CodePen.

5.thenメソッドは必ずPromiseを返却する

みてきたとおり、Promiseのthenメソッドには、Promiseが成功した(fulfilled状態)ときのコールバック関数と、失敗(rejected状態)ときのコールバック関数を指定できる。

コールバック関数がどんな値をreturnしようとも、thenメソッド自体は必ず Promise を返す。これを忘れない。

  • 以下は、値"value5"をもつfulfilled状態のPromiseを最初のthenの第1引数に指定したfulfilled用のコールバック関数内で処理している。
  • このコールバック関数で"value5-1"という値を返すと、thenメソッドは値"value5-1"を持ち、fulfilledなPromiseを返す
const p5 = Promise.resolve('value5');
p5
  .then((value) => { // ←このthenメソッドの返値は 値"value5-1"をもつfulfilledなPromiseとなる。つまり、Promise.resolve("value5-1")と同じ。
    console.log(`Promise(p5)はresolveされました 値:${value}`);

    // この「Promise(p5)がresolveされたとき用のコールバック関数」で
    // "value5-1"という値を返す。
    return 'value5-1';
  })
  .then((value) => {
    console.log(`前のPromiseはresolveされました 値:${value}`);
  });

See the Pen Promiseサンプル5 by Tom Misawa (@riversun) on CodePen.

5.1 thenメソッドに指定したコールバック関数が何も返さないと、thenメソッドは"undefined"な値を持つPromiseが返す

const p51 = Promise.resolve('value51');
p51
  //このthenの返値は Promise.resolve() とおなじく、値が"undefined"でfulfilled状態のPromise
  .then((value) => {
    // コールバック関数内でなにもreturnしない場合は
    // 値がundefinedでfulfilled状態のPromiseがこのthenの返値となる

    // なにも返さない
  })
  .then((value) => {
    console.log(`前のPromiseはresolveされました 値:${value}`);
  });

See the Pen Promiseサンプル6 by Tom Misawa (@riversun) on CodePen.

5.2 thenメソッドに指定したコールバック関数でPromiseを返すと、thenはコールバック関数の返値のPromiseの実行結果と同じ値・状態のPromiseを返す


const p52 = Promise.resolve('value52');
p52
  .then((value) => {

    // thenメソッドに指定したコールバック関数でPromiseを返すと、
    // そのthenメソッドの返値は、コールバック関数が返したPromiseと同じ状態、同じ値のものが返る

    //このPromiseは、非同期実行後にrejectedなPromiseをかえす
    return new Promise((resolve, reject) => {
      console.log('非同期処理を実行中。');
      setTimeout(() => {
        reject(value + '-rejected');
      }, 2000);
    });
  })
  .then((value) => {
    console.log(`前のPromiseはresolveされました 値:${value}`);
  }, (value) => {
    console.log(`前のPromiseはrejectされました 値:${value}`);
  })

See the Pen Promiseサンプル52 by Tom Misawa (@riversun) on CodePen.

5.3 thenメソッドやcatchメソッドの返値としてrejectedなPromiseを返したい場合は、引数に指定したコールバック関数内で throw ErrorするかrejectedなPromiseを返すかどちらか

5.3.1 Errorをスローしてrejectedにする

thenメソッドの引数に指定したコールバック関数内でErrorをスローすると、thenメソッドは「値にErrorをもつrejectedなPromise」を返す

const p53 = Promise.resolve('value53');
p53
  .then((value) => {
    // コールバック関数内で
    // Errorをスローすると、thenは値がErrorで、rejectedなPromiseを返す
    throw Error('error53');
  })
  .then((value) => {
      //よばれない
      console.log(`前のPromiseはresolveされました 値:${value}`);
    },
    (value) => {
      console.log(`前のPromiseはrejectされました 型:${Object.prototype.toString.call(value)}`);
      console.log(`前のPromiseはrejectされました 値:${value}`);
    });

See the Pen Promiseサンプル53 part1 by Tom Misawa (@riversun) on CodePen.

5.3.2 Promise.reject()を返してrejectedにする

thenメソッドの引数に指定したコールバック関数でPromise.reject()を返すと、thenメソッドは「rejectの引数に指定した値にをもつrejectedなPromise」を返す

const p53part2 = Promise.resolve('value53part2');
p53part2
  .then((value) => {

    // コールバック関数内で
    // rejectedなPromiseを返すと、thenはrejectedなPromiseを返す
    return Promise.reject('error53part2')

  })
  .then(
    (value) => {
      //よばれない
      console.log(`前のPromiseはresolveされました 値:${value}`);
    },
    (value) => {
      console.log(`前のPromiseはrejectされました 型:${Object.prototype.toString.call(value)}`);
      console.log(`前のPromiseはrejectされました 値:${value}`);
    });

See the Pen Promiseサンプル53part2 by Tom Misawa (@riversun) on CodePen.

5.3.3 Promise内の非同期処理でrejectして、rejectedにする

const p53part3 = Promise.resolve('value53part3');
p53part3
  .then((value) => {

    // thenに指定したコールバック関数内でrejectedなPromiseを返す
    return new Promise((resolve, reject) => {
      console.log('非同期処理を実行中。');
      setTimeout(() => {
        // 何かのエラー発生
        reject("error53part3");// ←この処理によって値"error53part3"をもつrejectedなPromiseとなる
      }, 2000);
    });

  })
  .then(
    (value) => {
      console.log(`前のPromiseはrejectされました 型:${Object.prototype.toString.call(value)}`);
      console.log(`前のPromiseはresolveされました 値:${value}`);
    },
    (value) => {
      console.log(`前のPromiseはrejectされました 型:${Object.prototype.toString.call(value)}`);
      console.log(`前のPromiseはrejectされました 値:${value}`);
    });

See the Pen Promiseサンプル53 part3 by Tom Misawa (@riversun) on CodePen.

6.Promiseをつかった直列実行

  • 3つのAPIを順番に呼び出していく例を考える
  • 呼び出し順序に意味がある前提で、(その気になれば)前のAPIの呼び出し結果を次のAPIでも使えるようにする

実験用に、処理に時間のかかるAPI(スタブ)を3つつくる。Web API呼び出してるつもり。
3つは、それぞれ 足し算、引き算、かけ算 ができる

// 処理に時間がかかる足し算API
function webApiAdd(a, b) {
  return new Promise((resolve, reject) => {
    // 処理に1秒かかる足し算APIをエミュレート
    setTimeout(() => {
      resolve(a + b);
    }, 1000);
  });
};

// 処理に時間がかかる引き算API
function webApiSub(a, b) {
  return new Promise((resolve, reject) => {
    // 処理に1秒かかる引き算APIをエミュレート
    setTimeout(() => {
      resolve(a - b);
    }, 1000);
  });
};

// 処理に時間がかかるかけ算API
function webApiMult(a, b) {
  return new Promise((resolve, reject) => {
    // 処理に2秒かかるかけ算APIをエミュレート
    setTimeout(() => {
      resolve(a * b);
    }, 2000);
  });
};

6.1 Promiseで非同期APIの直列呼び出し

  • 前段の呼び出し結果を後段で活用できるようにする
  • async/awaitはのちほど、ここではPromiseでがんばる
  • ポイントはthenの中で呼び出しをネストさせないこと
    (ネストを深くしてしまうと、せっかくのPromiseがもったいない)
足し算→引き算→掛け算の順番に実行して、すべての結果を返す関数

// 足し算→引き算→掛け算の順番に実行して、すべての結果を詰めるAPI
function webApi(a, b) {

  const container = {};
  return Promise.resolve(container)
    .then(container => {
      console.log('足し算APIを呼び出し中');
      return webApiAdd(a, b)
        .then((addResult) => {
          container.addResult = addResult;
          return container;
        })//rejectが発生しても、ここで .catchせずにreject状態のpromiseをそのまま返す仕様
    })
    .then((container) => {
      console.log('引き算APIを呼び出し中');
      return webApiSub(a, b)
        .then((subResult) => {
          container.subResult = subResult;
          return container;
        })
    })
    .then((container) => {
      console.log('かけ算APIを呼び出し中');
      return webApiMult(a, b)
        .then((multResult) => {
          container.multResult = multResult;
          return container;
        })
    });
}

呼び出しコードは


webApi(2, 3)
  .then((result) => {
    console.log(`すべての処理に成功しました 結果:${JSON.stringify(result)}`);
  })
  .catch((result) => {
    console.log(`途中で処理に失敗しました 結果:${JSON.stringify(result)}`);
  })

See the Pen Promiseサンプル61 by Tom Misawa (@riversun) on CodePen.

APIの順次呼び出し途中でエラーが発生した場合はどうなるか。

以下のように引き算API実行中にエラーが発生しまうことにする


function webApiSub(a, b) {
  return new Promise((resolve, reject) => {
    // 処理に1秒かかる引き算APIをエミュレート
    setTimeout(() => {
      reject("引き算APIでエラー発生");
    }, 1000);
  });
};

この状態でさきほどのコード↓を再度呼び出す


webApi(2, 3)
  .then((result) => {
    console.log(`すべての処理に成功しました 結果:${JSON.stringify(result)}`);
  })
  .catch((result) => {
    console.log(`途中で処理に失敗しました 結果:${JSON.stringify(result)}`);
  })

途中の引き算API呼び出しの失敗でrejectされる。

See the Pen Promiseサンプル61part2 by Tom Misawa (@riversun) on CodePen.

6.2 Promiseで非同期APIの並列呼び出し

Promise.allをつかうと、Promiseの並列処理ができる

先ほどの処理をPromise.allで並列処理に書き換える。
直列にくらべて、シンプルになる。

function webApiConcurrent(a, b) {
  return Promise.all([
    webApiAdd(a, b),
    webApiSub(a, b),
    webApiMult(a, b)])
    .then((results) => {
      const container = {};
      container.addResult = results[0];
      container.subResult = results[1];
      container.multResult = results[2];
      return container;
    })
}

呼び出しコードはさきほど同様以下のとおり

 webApiConcurrent(2, 3)
    .then((result) => {
      console.log(`すべての処理に成功しました 結果:${JSON.stringify(result)}`);
    })
    .catch((result) => {
      console.log(`途中で処理に失敗しました 結果:${JSON.stringify(result)}`);
    });

See the Pen Promiseサンプル62 by Tom Misawa (@riversun) on CodePen.

6.3 async/awaitを使った直列実行

Promiseついでに、async/awaitも書いておく

  • async

    • "async"を関数の冒頭につけた、async functionを宣言すると、その関数は Promise を返すようになる
    • async function内で return "value1";とすると、return Promise.resolve("value1"); をしたのと同じこと
  • await

    • async functionの中で、awaitをつけたPromiseがfulfilledまたはrejectedになるまで実行を止める(待つ)

さきほどの非同期APIリクエストの直列実行をasyncとawaitをつかって書き直すと以下のようになる。同期呼び出しのようにシンプルになった。

async function webApi2(a, b) {
  const container = {};
  console.log('足し算APIを呼び出し中');
  container.addResult = await webApiAdd(a, b);

  console.log('引き算APIを呼び出し中');
  container.subResult = await webApiSub(a, b);

  console.log('かけ算APIを呼び出し中');
  container.multResult = await webApiMult(a, b)

  return container;
}

呼び出しコードは、

 webApi2(2, 3)
    .then((result) => {
      console.log(`すべての処理に成功しました 結果:${JSON.stringify(result)}`);
    })
    .catch((result) => {
      console.log(`途中で処理に失敗しました 結果:${JSON.stringify(result)}`);
    });

See the Pen Promiseサンプル63 by Tom Misawa (@riversun) on CodePen.

6.4 async/awaitを使った並列実行

次は async/awaitを使った並列実行。といっても、並列実行自体はPromise.allにやらせる。Promise.allは実行結果をPromiseで返すので、そこでawaitによる待ちをいれているだけ。

async function webApi2Concurrent(a, b) {
  const results=await Promise.all([
    webApiAdd(a, b),
    webApiSub(a, b),
    webApiMult(a, b)]);
  const container = {};
  container.addResult = results[0];
  container.subResult = results[1];
  container.multResult = results[2];
  return container;
}

呼び出しは、以下のとおり

webApi2Concurrent(2, 3)
  .then((result) => {
    console.log(`すべての処理に成功しました 結果:${JSON.stringify(result)}`);
  })
  .catch((result) => {
    console.log(`途中で処理に失敗しました 結果:${JSON.stringify(result)}`);
  })

See the Pen Promiseサンプル64 by Tom Misawa (@riversun) on CodePen.

おまけ

Promiseつかってるとアロー関数「()=>{}」もよく出てくる。
記述量は少いが、そのぶんは人間の脳内補完(または思い出し)がたより

Promiseでのアロー関数

// 同じ意味。正解はLinter次第。

const p7 = new Promise((resolve, reject) => {
  resolve('value60');
});

const p7 = new Promise((resolve, reject) => resolve('value7'));

const p7 = new Promise(resolve => resolve('value7'));

thenメソッドでのアロー関数

// 以下は同じ意味
p7
  .then((value) => {
    return `${value}_edited`;
  })
  .then((value) => {
    console.log(value);
  });

p7
  .then((value) => `${value}_edited`) // returnは省略できる
  .then((value) => console.log(value)); //{}は省略できる

まとめ

  • Promise、async/await系の忘れやすい挙動をメモしました。
  • 何か忘れたときに、また追加します。

関連記事

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6