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はこのようにして非同期処理を実行することができます。