Promise の使い方がやっと少しつかめたのですが、きっとまた忘れてしまうので、忘れてしまった自分用に解説する記事を書いておきます。
ちなみに JavaScript の非同期処理では async / await がよく使われると思いますが、これは Promise をきれいに書くためのシンタックスシュガー(糖衣構文)で、何か新しいことをしているわけではありません。よって、まずは Promise を理解することが大切だと思いました。
目次
1. 非同期処理と同期処理
2. Promiseの基本
3. Promiseを使う
4. Promiseを作る
5. 参考
1. 非同期処理と同期処理
Promise は、非同期処理で使うものです。
まずは非同期処理と同期処理について整理しておきます。
同期処理
非同期処理の前に、まずは同期処理について説明します。同期処理は、プログラムを上から順番に実行していく処理のことです。プログラムに書いたとおりの順番で実行されていくので、処理の流れがわかりやすいです。
ただ途中で時間のかかる処理があった場合でも、それが終わるまでは次の処理には進みません。その後の全てのプログラムがいったん止まってしまいます。
これは場合によっては非効率的ですし、使っている人を待たせてしまう可能性があります。
非同期処理
そこで便利なのが、非同期処理です。
プログラムに非同期処理が書かれているとき、その非同期処理がまだ完了していなくても、プログラムはその後に書かれた処理に進んでいきます。
非同期処理は、外部から情報を取得する場合など、時間がかかる処理に使われます。非同期処理のプログラムが情報を取りに行っている間、後に書かれたプログラムたちはそれをじっと待つのではなく、先に自分たちの仕事を進めるという感じです。
プログラムの実行が止まらないので、効率的に処理を進めることができます。
非同期処理の動き
JavaScript は通常は同期処理を行いますが、非同期処理にも対応しています。
例えば JavaScript の setTimeout
は、非同期処理のメソッドです。これを使って、実際に非同期処理の動きをみていきます。
setTimeout
メソッドは、タイマーのようなものです。第一引数に行いたい処理を、第二引数に時間(ミリ秒)を渡します。すると第二引数に渡した時間経過後に、第一引数で渡した処理を実行してくれます。
setTimeout
は非同期処理のメソッドなので、setTimeout
の後に書いたプログラムは setTimeout
の完了を待たずに、処理を実行します。
setTimeout(() => {
console.log('setTimeoutの中に書いた処理です。指定の時間経過後に実行されます。');
}, 1000);
console.log('setTimeoutの後に書いた処理です。');
実行してみると、以下のように出力されます。
setTimeoutの後に書いた処理です。
# 1000ミリ秒後...
setTimeoutの中に書いた処理です。指定の時間経過後に実行されます。
setTimeout
を先に書いているのにも関わらず、setTimeout
の中に書いた処理よりも先に、setTimeout
の後に書いた処理が実行されていますね。これが非同期処理の動きです!
同期処理ではこのような事は起こらず、必ず前に書いた処理を待ってから、後の処理に進みます。
非同期処理で考えるべきこと
このように、時間がかかる処理を非同期処理とすることで、プログラムの実行を止めずにすむので便利です。ですが、非同期処理を扱う場合、考えなければいけないことがあります。
例えば、外部から情報を取得してその情報を画面に表示したい場合を考えます。非同期処理で外部に情報を取っていっている間に、次の処理(情報を画面に表示する処理)に進んでしまったら、まだ表示する情報がないので何も表示されない!ということが起きてしまいます。
なので、外部に情報を取りに行っている間もプログラム自体は止めたくないけど、"取りに行っているその情報を使う処理"は、情報が取得できるまでは止めておきたいですよね。非同期処理を扱う場合は、このような制御を適切に行う必要があります。
2. Promiseの基本
非同期処理をうまく扱うためのものが Promise です。Promise は、JavaScript のオブジェクトです。
Promise には 3 つの状態があります。Promise オブジェクトを作った時点では、pending (保留中)
です。これから非同期処理を行うにあたり、まだ成功するか失敗するかわからない状態です。
そして非同期処理を行い、それが成功したら fulfilled (成功)
、失敗したら rejected (失敗)
という状態になります。
非同期処理の関数の中で Promise を return することで、関数の呼び出し元では Promise を受け取れます。その Promiseの状態が fulfilled
なら取得できた値で何かする、rejected
ならエラーメッセージを表示する、pending
の状態なら次の処理に進まずに待つ、といったように処理を分けることができます。
3. Promiseを使う
以下に Promise を返す関数 fakeRequest
があります。
いったんこの関数の構文は気にしないでください。まずは呼び出し元から Promise をどう使うかを見ていきます。
fakeRequest
がどんな関数かというと、リクエスト先の URL を受け取って、レスポンスを返す動きをダミーで表現したものです。レスポンスが返ってくるまでの待ち時間 delay
をランダムに生成して、2 秒以下ならリクエスト成功、2 秒より多くかかったらリクエスト失敗となります。
const fakeRequest = (url) => {
return new Promise((resolve, reject) => {
const delay = Math.random() * 3000;
setTimeout(() => {
if (delay > 2000) {
reject();
} else {
resolve();
}
}, delay)
});
}
fakeRequest
関数の構文はまだ気にしなくてOKですが、関数の中で、return new Promise
としていることに注目してください。fakeRequest
を呼び出すと、Promise オブジェクトが return されてくるということです。
delay (ランダムに生成した待ち時間) が 2 秒以下なら fulfilled (成功)
、2秒より多ければ rejected (失敗)
の状態の Promise が return されてくるという仕組みになっています。
では fakeRequest
を呼び出して、return されてくる Promise を使ってみましょう。
どう使うかというと、return されてきた Promise オブジェクトに、成功したときと失敗したときの処理を登録します。
const request = fakeRequest('https://dummy/1/');
request.then(() => {
console.log('リクエストに成功しました!');
}).catch(() => {
console.log('リクエストに失敗しました...');
});
上のコードの流れを説明します。
- 1 行目:呼び出した
fakeRequest
から return されてきた Promise オブジェクトが 変数request
に入ります。この Promise オブジェクトは、リクエストが成功していたらfulfilled (成功)
、失敗していたらrejected (失敗)
の状態になっています。
この Promise オブジェクトに、次に行いたい処理を登録します。 - 2, 3 行目:
request.then(() => ...
の部分に、処理が成功した場合の処理を書きます。 - 4, 5 行目:
request.catch(() => ...
の部分に、処理が失敗した場合の処理を書きます。
このように、処理が成功したときつまり return されてきた Promise オブジェクトが fulfilled (成功)
であれば then
、処理が失敗したときつまり return されてきた Promise オブジェクトが rejected (失敗)
であれば catch
の処理につなぐことができます!
上のコードでは Promise オブジェクトを request
という変数に入れてから、request.then(() => ...
として処理をつないでいますが、fakeRequest
を呼び出して返ってくる Promise オブジェクトに直接つなぐ書き方ができます。以下のように書けます。
fakeRequest('https://dummy/1/')
.then(() => {
console.log('リクエストに成功しました!');
})
.catch(() => {
console.log('リクエストに失敗しました...');
});
では、リクエストを何個か順番に投げたい場合を考えます。リクエストが成功した場合に、さらに次のリクエストを fakeRequest
に投げる処理を書いてみます。
fakeRequest('https://dummy/1/')
.then(() => {
console.log('リクエスト1に成功しました!');
fakeRequest('https://dummy/2/')
.then(() => {
console.log('リクエスト2に成功しました!');
fakeRequest('https://dummy/3/')
.then(() => {
console.log('リクエスト3に成功しました!');
}).catch(() => {
console.log('リクエスト3に失敗しました...');
});
}).catch(() => {
console.log('リクエスト2に失敗しました...');
});
}).catch(() => {
console.log('リクエスト1に失敗しました...');
});
階層が深くなってごちゃごちゃしてきました。これは Promise の真の力を使って、きれいに書くことができます。どうするかというと、then の中で Promise を return すると、次の then につなげることができるのです。
fakeRequest
を呼び出すと Promise が return されてくるのでしたね。なので以下のように書くことができます。
fakeRequest('https://dummy/1/')
.then(() => {
console.log('リクエスト1に成功しました!');
return fakeRequest('https://dummy/2/'); // ここ! then の中で Promise を return すると次の then へ
})
.then(() => {
console.log('リクエスト2に成功しました!');
return fakeRequest('https://dummy/3/'); // ここ! then の中で Promise を return すると次の then へ
})
.then(() => {
console.log('リクエスト3に成功しました!');
})
.catch(() => {
console.log('リクエストに失敗しました...');
});
きれいに書けました。Promise を使う側からの基本の使い方はこのような感じです。
4. Promiseを作る
ここまでは Promise を使う側でしたが、次は Promise を作っている方の関数を見ていきます。
先ほども使った fakeRequest
関数を見てみます。
const fakeRequest = (url) => {
return new Promise((resolve, reject) => {
const delay = Math.random() * 3000;
setTimeout(() => {
if (delay > 2000) {
reject();
} else {
resolve();
}
}, delay)
});
}
上のコードの構文を説明します。
2 行目で new Promise
として Promise オブジェクトを作り、return しています。
new Promise
としたとき、第一引数には処理が成功したときの関数名、第二引数には処理が失敗したときの関数名を指定します。名前は何でもよいのですが、一般的に resolve と reject が使われます。
そして、処理が成功したときにresolve、失敗したときにrejectを実行するように関数内に書いていきます。 上のコードでも、delay > 2000
なら reject、そうでなければ resolve を実行していますね。
resolveが実行されると Promise の状態が fulfilled (成功)
になります。reject が実行されるとPromise の状態が rejected (失敗)
になります。そしてその Promise オブジェクトが呼び出し元に return されて、then
や catch
につながっていくというわけです。
非同期処理関数から値を返す
さて、この fakeRequest
関数はリクエストを受け取ってレスポンスを返す動きをダミーで表現したものですが、今のコードでは何も値を返していない状態です。本来であれば、呼び出し元に返すレスポンスの内容が何かしらあるはずです。
値を返せるように、以下のようにコードを変更します。
処理が成功したときは resolve を実行して、resolve に渡した値は呼び出し元の then
に渡ります。
処理が失敗したときは reject を実行して、reject に渡した値は呼び出し元の catch
に渡ります。
const fakeRequest = (url) => {
return new Promise((resolve, reject) => {
const delay = Math.random() * 3000;
setTimeout(() => {
if (delay > 2000) {
reject('コネクションタイムアウト'); // ここ! 呼び出し元の catch に値を渡す
} else {
resolve(`ダミーデータ(${url})`); // ここ! 呼び出し元の then に値を渡す
}
}, delay)
});
}
resolve や reject に渡した値は、呼び出し元で受け取って使うことができます。
then
や catch
の引数で受け取ります。
fakeRequest('https://dummy/1/')
.then((data) => { // ここ! 呼び出し先の関数から値を受け取るための引数を追加
console.log('リクエスト1に成功しました!');
console.log(data); // ここ! 受け取った値を使う 以下同様
return fakeRequest('https://dummy/2/');
})
.then((data) => {
console.log('リクエスト2に成功しました!');
console.log(data);
return fakeRequest('https://dummy/3/');
})
.then((data) => {
console.log('リクエスト3に成功しました!');
console.log(data);
})
.catch((err) => {
console.log('リクエストに失敗しました...');
console.log(err);
});
5. 参考
この記事は、Udemy の【世界で70万人が受講】Web Developer Bootcamp 2023(日本語版)より、セクション27. 非同期なJavaScript!の内容を参考にまとめました。