概要
- 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関数でセットされた値) を持つ とおぼえておく。
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 functionを宣言すると、その関数は Promise を返すようになる
- async function内で
return "value1";
とすると、return Promise.resolve("value1");
をしたのと同じこと
-
- 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系の忘れやすい挙動をメモしました。
- 何か忘れたときに、また追加します。