はじめに
Promise
の解説サイトを見ると「resolve関数を呼ぶとfulfilledになります」といった表現が使われるなど、ちゃんとした状態遷移の説明がないことがあったので、自分の理解用に状態遷移図を作成してみました。基本的に勉強したのは「Promise
コンストラクター」のみです(参考URL)。独学のため間違っているところがあるかもしれません。
状態遷移図
Promise
には排他的な3つの状態があります。
- 全体として、正確さよりも理解のしやすさを優先して表現しています(実際に理解しやすくなっているかは別ですが)。また、状態確定など、この記事内でしか出てこない単語もあるため参考URLと見比べながら理解しようとすると混乱する可能性があります。
-
unresolved、resolved、locked in、settledについては説明用であり、「
Promise
がsetteledになる」というような言い方はしても、Promise
の状態が実際にsetteledになる(pending、fulfilled、rejected以外の状態になる)ことはありません。 -
unresolved、resolvedについては
States
ではなくFates
と表現されており、またfulfilledやsettledがit is fulfilled
、it is settled
などのように記載されているのに対し、locked inについてはit has been "locked in"
と記載されています。そのため、これら(3つの状態以外)を同じように扱うのは好ましくないかもしれませんが、概要把握を優先し状態として扱っています。
イメージの説明
例えばPromise
の処理が「ファイルの中身を読み取ってその内容を返す処理」の場合
-
処理の全てを自分で行う場合
- 処理がすべて正常に動作した場合 → 処理は完全に成功(「ファイルの中身を読み取ってその内容を返す処理」を行うという約束は履行された)
unresolved → (b) → fulfilled - 処理が途中で失敗した場合 → 処理失敗(「ファイルの中身を読み取ってその内容を返す処理」を行うという約束は拒否された)
unresolved → rejected
- 処理がすべて正常に動作した場合 → 処理は完全に成功(「ファイルの中身を読み取ってその内容を返す処理」を行うという約束は履行された)
-
ファイルのパスの取得だけを自分で処理し、パスからファイルの中身を読み取って内容を返す処理は別の
Promise
の処理とした(別のPromise
に任せた)場合- パスの取得が成功し、別の処理も成功した場合 → 処理は完全に成功
unresolved →(a)→ locked in(別の処理実行中) → 別の処理成功で fulfilled - パスの取得は成功したが、別の処理が失敗した場合 → 処理失敗
unresolved →(a)→ locked in(別の処理実行中) → 別の処理失敗で rejected - パスの取得が失敗した場合 → 処理失敗
unresolved → rejected
- パスの取得が成功し、別の処理も成功した場合 → 処理は完全に成功
サンプルコード
resolve関数の引数がプリミティブの場合(数値や文字列など)やthenable
でない場合は、直ぐにfulfilledになります。
-
thenable
についての詳細はこちらを参照。つまりPromise
オブジェクトと似たようなオブジェクトのことでPromise
オブジェクトもthenable
になります。よくわからない場合、まずはthenable
をPromise
オブジェクトに置き換えて読んでみてください。
const promise1 = new Promise((resolve, reject) => {
resolve(10);
});
console.log(promise1);
//Promise { <state>: "fulfilled", <value>: 10 }
const promise1 = new Promise((resolve, reject) => {
resolve(console);
});
console.log(promise1);
//Promise { <state>: "fulfilled", <value>: Console }
resolve関数の引数がthenable
の場合は、そのthenable
の状態に依存します。thenable
がfulfilledになると、自身もfulfilledになります。
const promise1 = new Promise((resolve, reject) => {
resolve(
new Promise((resolveInner, rejectInnner) => {
setTimeout(() => resolveInner(), 2000);
})
);
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//Promise { <state>: "fulfilled", <value>: undefined }
//Promise { <state>: "fulfilled", <value>: undefined }
//...
thenable
がrejectedになると、自身もrejectedになります。
const promise1 = new Promise((resolve, reject) => {
resolve(
new Promise((resolveInner, rejectInnner) => {
setTimeout(() => rejectInnner(), 2000);
})
);
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//Promise { <state>: "rejected", <reason>: undefined }
//Uncaught (in promise) undefined
//Promise { <state>: "rejected", <reason>: undefined }
//...
なお、非同期のエラー処理については注意が必要です。以下はfulfilledになります。
JavaScriptと非同期のエラー処理 - Yahoo! JAPAN Tech Blog
const promise1 = new Promise((resolve, reject) => {
resolve(
new Promise((resolveInner, rejectInnner) => {
setTimeout(() => { throw new Error("error") }, 500);
setTimeout(() => resolveInner(), 2000);
})
);
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Uncaught Error: error
//Promise { <state>: "pending" }
//Promise { <state>: "fulfilled", <value>: undefined }
//Promise { <state>: "fulfilled", <value>: undefined }
//...
resolve関数の引数がthenable
だとしても、それが自分自身の場合はrejectedになります。
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(promise1));//引数が自分自身
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Uncaught (in promise) TypeError: A promise cannot be resolved with itself.
//Promise { <state>: "rejected", <reason>: TypeError }
//Promise { <state>: "rejected", <reason>: TypeError }
//...
resolve関数の引数がthenable
だとしても、then
プロパティにアクセスすると例外を発生させるような場合はrejectedになります。この場合、resolvedになる前に(unresolvedの状態で)例外が発生したのでrejectedになったわけではなく、また引数のthenable
の状態がrejectedになったのに伴いrejectedになったわけでもないです*。
const promise1 = new Promise((resolve, reject) => {
resolve({
get then() {
throw new Error("error");
},
});
});
console.log(promise1);
//Promise { <state>: "rejected", <reason>: Error }
reject関数が呼ばれると、直ぐにrejectedになります。
const promise1 = new Promise((resolve, reject) => {
reject();
});
console.log(promise1);
//Promise { <state>: "rejected", <reason>: undefined }
//Uncaught (in promise) undefined
例外が発生すると、直ぐにrejectedになります。
const promise1 = new Promise((resolve, reject) => {
throw new Error('error');
});
console.log(promise1);
//Promise { <state>: "rejected", <reason>: Error }
//Uncaught (in promise) Error: error
処理するものがなくなっても成功(または失敗)にはなりません。以下はずっとpendingのままです。
const promise1 = new Promise((resolve, reject) => {
//処理がないため即終了
});
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//...
一度処理が成功もしくは失敗した(resolvedになった)後に、resolve関数やreject関数が呼ばれたり、例外が発生したりしても、状態に影響を与えることも結果の値が変わることもありません。影響するのは処理が成功も失敗もしていないとき(unresolvedのとき)に呼ばれたり、発生したりしたときです。
const promise1 = new Promise((resolve, reject) => {
resolve(//呼ばれた時点でresolvedになり引数↓のPromiseにロックインされる。
new Promise((resolveInner, rejectInnner) => {
setTimeout(() => resolveInner(), 2000);
})
);
reject();//ここでrejectを呼んでも状態は変化しない。
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//Promise { <state>: "fulfilled", <value>: undefined }
//Promise { <state>: "fulfilled", <value>: undefined }
//...
const promise1 = new Promise((resolve, reject) => {
resolve(//呼ばれた時点でresolvedになり引数↓のPromiseにロックインされる。
new Promise((resolveInner, rejectInnner) => {
setTimeout(() => resolveInner(20), 2000);
})
);
resolve(10);//ここの10は影響を与えない。
});
console.log(promise1);
setInterval(() => console.log(promise1), 1000);
//Promise { <state>: "pending" }
//Promise { <state>: "pending" }
//Promise { <state>: "fulfilled", <value>: 20 }
//Promise { <state>: "fulfilled", <value>: 20 }
//...
ただし、resolvedになることと処理が止まることは別問題です。
const promise1 = new Promise((resolve, reject) => {
resolve();//この時点でresolvedになりfulfilled(settled)。
console.log("このコメントは出力される");//無効になるわけではない。
});
console.log(promise1);
//このコメントは出力される
//Promise { <state>: "fulfilled", <value>: undefined }
参考)コンストラクタについて
new
によりPromise
オブジェクトを返します。
new Promise(executor)
executor
は関数オブジェクトであり、resolveFunc
とrejectFunc
の2つの引数を持ちます。通常executor
は以下のようになります。
function executor(resolveFunc, rejectFunc) {
//処理
}
より具体的に記載すると、例えば以下のようなコードになります。executor
はアロー関数にしています。つまりコンストラクタはresolveFunc
とrejectFunc
の2つの引数を持つわけではなく、resolveFunc
とrejectFunc
の2つの引数を持つexecutor
を引数に持つということになります。
const promise1 = new Promise((resolve, reject) => {
try{
//何らかの処理
resolve(value);//処理が成功した場合resolveFunc(ここではresolve)を呼ぶ
}catch(reason){
reject(reason);//処理が失敗した場合rejectFunc(ここではreject)を呼ぶ
}
});