Promiseが導入される前は、コールバック関数を使って非同期処理を制御していました。
しかし、コールバック関数をつなげた場合、コードの階層が深くなり、可読性が下がってしまうという問題がありました。
そこで考えられたのがPromiseです。
Promiseを使うことでより直感的に非同期処理を記述できるようになります。
Promiseとは
非同期処理をより簡単に、可読性が上がるようにした記述方法です。
Promiseは、以下の3つのステータスを持っています。
- Pending:初期の状態、または処理待ちの状態
- Fullfilled:処理が成功して完了した状態
- Rejected:処理が失敗した状態
Promiseオブジェクトのステータスによって、処理を分岐していきます。
Promiseの構文
new Promise((resolve,reject) => {
// 同期処理
resolve() // もしくは reject()
})
.then(() => {
// 非同期処理(resolveを待つ)
})
.catch(() => {
// 非同期処理(rejectを待つ)
})
.finally(() => {
// 非同期処理(then、またはcatchを待つ)
});
Promiseの基本形です。new PromiseでPromiseをインスタンス化します。インスタンス化したPromiseのthenメソッド、catchメソッド、finallyメソッドを使って、非同期処理に対して制御を加えていきます。
Promise構文の中では、new Promiseの引数に与えたコールバック関数は同期処理されますが、thenメソッドやcatchメソッド、finallyメソッドは全て非同期で実行されます。
Promiseの使い方(resolve, then)
new Promise((resolve, reject) => {
resolve('hello');
})
.then((data) => {
console.log(data); // hello
})
.catch(() => {
// スキップされる
})
.finally(() => {
console.log('end'); // end
});
Promiseの引数として、コールバック関数を設定します。コールバック関数はresolveとrejectの2つの引数を取ります。
resolveが呼ばれた場合は、thenメソッドの中のコールバック関数が実行され、このコールバック関数の引数にはresolveで渡した実引数が渡ってきます。
thenメソッドが完了すると、catchメソッドはスキップされ、finallyメソッドに処理が移ります。finallyメソッドでは終了処理を記述します。
Promiseの使い方(reject, catch)
new Promise((resolve, reject) => {
reject('bye');
})
.then(() => {
// スキップされる
})
.catch((data) => {
console.log(data); // bye
})
.finally(() => {
console.log('end'); // end
});
rejectは、Promiseのコールバックでエラーが発生したときにPromiseに通知するために使用する関数です。
rejectが呼ばれた場合、catchメソッドの中のコールバック関数が実行され、引数にはrefectで渡した実引数が渡ってきます。
catchメソッドの中のコールバック関数が実行された後に、finallyメソッドに処理が移ります。
Promiseの使い方(finally)
finallyメソッドに渡したコールバック関数は必ず実行されるため、共通の終了処理を記述します。
また、finallyメソッドの中で使用するコールバック関数は引数を使用することができません。
Promiseの流れを確認する
ここからは、Promiseの流れを確認していきます。
new Promise((resolve, reject) => {
console.log('promise');
resolve();
}).then(() => {
console.log('then');
});
console.log('global');
// コンソール
// 1. promise
// 2. global
// 3. then
globalがthenよりも先に表示されます。thenメソッドに渡したコールバック関数は非同期処理なため、コールスタックが空になった後に実行されます。
thenを繋げてみる
new Promise((resolve, reject) => {
console.log('promise');
resolve();
})
.then(() => {
console.log('then');
})
.then(() => {
console.log('then');
})
.then(() => {
console.log('then');
})
.then(() => {
console.log('then');
});
console.log('global');
// コンソール
// 1. promise
// 2. global
// 3. then
// 4. then
// 5. then
// 6. then
thenを繋げると、thenメソッドに渡されたコールバック関数が順番に処理されます。
コールバック関数を使った非同期処理と比べて、thenを繋げても多階層になることはありません。
catchメソッドを追加してみる
new Promise((resolve, reject) => {
console.log('promise');
reject(); // resolve → rejectに変更
})
.then(() => {
console.log('then');
})
.catch(() => {
console.log('catch');
});
console.log('global');
// コンソール
// 1. promise
// 2. global
// 3. catch
resolveをrejectに変更すると、thenメソッドはスキップされcatchメソッドに処理が移ります。
catchメソッドも非同期処理されるため、コンソールの一番最後にcatchが表示されます。
thenの中でcatchに処理を移行したい場合
rejectはnew Promiseの引数に渡すコールバック関数内でしか使えません。
そのため、thenメソッドの中でcatchメソッドに処理を移行したい場合には、throwを使います。
new Promise((resolve, reject) => {
console.log('promise');
resolve();
})
.then(() => {
console.log('then');
throw new Error();
})
.then(() => {
console.log('then');
})
.catch(() => {
console.log('catch');
});
console.log('global');
// コンソール
// 1. promise
// 2. global
// 3. then
// 4. catch
1回目のthenは実行されますが、2回目のthenは実行されません。
throwでエラーが発生したことをPromiseに伝えることでcatchメソッドに処理が移行します。
finallyを追加してみる
finallyは、resolveの場合はthenメソッドの後、rejectの場合はcatchメソッドの後に必ず呼ばれます。
new Promise((resolve, reject) => {
console.log('promise');
resolve();
})
.then(() => {
console.log('then');
})
.catch(() => {
console.log('catch');
})
.finally(() => {
console.log('finally');
});
console.log('global');
// コンソール
// 1. promise
// 2. global
// 3. then
// 4. finally
thenメソッド(catchメソッド)のコールバック関数に引数を設定する場合
new Promise((resolve, reject) => {
console.log('promise');
resolve('hello');
})
.then((data) => {
console.log(`then: ${data}`);
return data;
})
.then((data) => {
console.log(`then: ${data}`);
return data;
})
.catch((data) => {
console.log(`catch: ${data}`);
})
.finally((data) => {
console.log(`finally: ${data}`);
});
console.log('global');
// コンソール
// 1. promise
// 2. global
// 3. then: hello
// 4. then: hello
// 5. filally: undefined
1つ目のthenメソッドのdataに値を渡すためにはroselveに引数を渡す必要があります。この例で言うと、helloがthenメソッドの引数として設定されます。
2つ目のthenメソッドのdataに値を渡すためには1つ目のthenメソッドの中でreturnを使用します。
ただし、finallyは引数を取れないのでthenメソッドの中でretrunを使用してもundefindとなります。
上記同様、rejectが呼ばれた場合はcatchメソッドに処理が移り、dataで値を受け取れます。
new Promise((resolve, reject) => {
console.log('promise');
reject('bye');
})
.then((data) => {
console.log(`then: ${data}`);
return data;
})
.then((data) => {
console.log(`then: ${data}`);
return data;
})
.catch((data) => {
console.log(`catch: ${data}`);
})
.finally((data) => {
console.log('finally');
});
console.log('global');
// コンソール
// 1. promise
// 2. global
// 3. catch: bye
// 4. finally
PromiseにsetTimeout(非同期関数)を使ってみる
Promoseを使うとresolveかrejectが呼ばれるまではthenやcatchが呼ばれません。
そのため、Promiseのコールバック関数に非同期関数を組み込むことで非同期をうまく制御することができます。
new Promise((resolve, reject) => {
console.log('promise');
resolve('hello');
})
.then((data) => {
console.log(`then: ${data}`);
return data;
})
.then((data) => {
console.log(`then: ${data}`);
return data;
})
.catch((data) => {
console.log(`catch: ${data}`);
})
.finally((data) => {
console.log(`finally: ${data}`);
});
console.log('global');
// コンソール
// 1. promise
// 2. global
// ...1秒後
// 3. then: hello
// 4. then: hello
// 5. filally: undefined
1秒間経った後にresolveが呼ばれるので、後続の処理はresolveが呼ばれた1秒後に実行されます。
resolveをrejectに変えた場合も同様です。
Promiseはこのようにして非同期処理を実行することができます。