もし2015年以降にウェブ開発を経験したことがあるなら、おそらくPromiseという概念を聞いたことがあるでしょう。Promiseが現代のフロントエンドコードベースに広く使われていると言っても過言ではありません。しかし、特にフロントエンド開発の経験があまりない多くのウェブ開発者は、Promiseの内部構造を十分に理解せずに使ってしまいました。その結果、Promiseの誤用やそれに伴うバグが数多く発生しています。この記事では、Promiseがなぜ作られたのか、Promiseがどのように機能するのか、そしてフロントエンド開発の経験があまりないと仮定して、どのようにコードを使うべきかを解説します。
シンクロニシティ:すべての始まり
最初のプログラミングの授業をまだ覚えていますか?先生はおそらく、コンピュータはコードを一行ずつ実行すると教えたでしょう。前の行の実行を終えなければ次の行に進まないのです。私たちはこのような連続的なコード実行を「同期的」と呼びます。論理的なシンプルさや可読性など、確かに利点があります。しかし、大きな欠点もあります。それは無駄遣いが多いということです。
例えば、あなたのコードに別のコンピュータシステムからデータを取得する操作が含まれているとします。データ検索は時間がかかりますが、計算を行うためにコード自体は必要ありません。もしコードが同期的であれば、実行するシステムはデータ取得が完了するまでアイドル状態にしなければならず、その結果、時間と計算資源が無駄になります。
非同期性:同期性のより時間効率の良い代替手段
同期プログラミングの時間効率の低さを是正するために、プログラミング界の先駆者たちは非同期性の概念を考案しました。同期性と異なり、非同期性は時間のかかり計算負荷の少ない操作をメインのプログラムフローから分岐させ、その後の操作処理のためのスペースを作ることを可能にします。このような新しいプログラミング方法は、特に大量のシステム間通信を伴う現代のコンピュータプログラムの時間性能を飛躍的に向上させました。
Callback:非同期性においてもシーケンシャルは依然として必要です
非同期性という素晴らしい概念ができた今、すべての操作を並列で実行させてはいけませんか?答えはノーです。なぜなら、依存関係が完了するまで他に依存する操作が始まらないからです。したがって、分岐した操作が完了時に依存関係をトリガーできるようにする方法を見つける必要があります。言い換えれば、非同期プログラミングにおける逐次性を表現する必要があります。これに対して、コンピュータサイエンスの先駆者たちはコールバック関数を考案しました。コールバック関数とは、別の関数に渡せる関数のことです。その別の関数では、特定の操作が終わった後にコールバック関数を実行できます。コールバック関数の助けを借りれば、時間のかかる操作を安全に脇に置くことができます。コールバック関数で指定された依存操作は完了後に再開されることを十分に理解しています。
以下はコールバック関数の使い方の例です。データベースからユーザー情報を取得する必要があるとしましょう。ユーザー情報を受け取った後、それをユーザーインターフェースに表示する必要があります。上記の2つの操作の順序的な関係は以下のコードを使って説明できます:
function getUserInfo(id, callback) {
setTimeout(() => {
もし(typeof id === "number") {
const userInfo = {
id、
名前:「アダム・スミス」
年齢:30歳
};
callback(userInfo);取得したユーザー情報でコールバック関数を呼び出す
} そうでなければ {
新しいError("Invalid ID")
}
}, 2000); 2秒の遅延をシミュレートします
}
function displayUserInfo(userInfo) {
console.log(ユーザー情報)
}
console.log(「ユーザー情報取得中...」);即時実行
getUserInfo(1, displayUserInfo)
ご覧の通り、'displayUserInfo'はコールバック関数です。これを「getUserInfo」に渡し、まずデータベースからユーザー情報を取得(「setTimeout」でシミュレート)し、その後取得したユーザー情報を引数として提供されたコールバック関数を呼び出します。このようにして、ユーザー情報が準備できた後にのみプログラムが「displayUserInfo」を呼び出すことを確実にできます。
コールバックヘル:コールバック関数の避けられない落とし穴
コールバックは、操作間の連続的な関係を表現する方法を提供します。しかし、それを誤用すると「コールバックヘル」と呼ばれる恐ろしい状況が生じることがあります。callbackhell.com によると、「コールバックヘルの原因は、人々がJavaScriptの実行を上から下まで視覚的に行うように書こうとするときです。」以下はその様子を示す例です。
function1(function2 () {
時間のかかる操作
function3(function4 () {
時間のかかる操作
function5(function6 () {
時間のかかる操作
})
})
})
ご覧の通り、『function1』を呼び出すと、『function2』をコールバックとして渡します。『function1』の実行が終わると、『function2』が実行を開始します。『function2』内でも、『function3』を呼び出して『function4』をコールバックとして渡すという似たような状況があります。同じ問題が『function4』にも起こっています。このような複雑なコード構造は、コード実行の順序に視覚的に従ったコールバックを書くことから生じ、さらに多くの関数が関与するとさらに悪化します。これによりコードの可読性と保守性の両方が損なわれます。
救出への約束!
「コールバック地獄を避ける方法はありますか?」と尋ねるかもしれません。コードをモジュール化してみることはできます。しかし、コールバック関数とそれを使う関数を完全に切り離すことは依然としてできません。もっと高度な代替手段はありますか?Promiseを紹介しましょう!JavaScriptにおいて、Promiseとは時間のかかる操作の最終的な完了または失敗とその結果を表します。これにより関数同士をネストする必要がなくなり、結果として読みやすく保守しやすいコードを作成できます。次に、Promiseがどのようにこれを実現しているかを実演します。
約束101:何かを返すと約束するけれど、100%確信はない
まずはシンプルな「Promise」オブジェクトの作り方を見てみましょう。
const promise = new Promise((resolve, reject) => {
コード
もし(isFulfilled) {
解決(結果)
} そうでなければ {
reject(エラー)
}
})
「Promise」オブジェクトを構築する際、関数を引数としてコンストラクタに渡します。この関数自体は2つの関数を引数として取ります。最初の引数は「リゾルブ」関数です。この関数の非同期操作の結果を引数として「リsolve」関数を呼び出し、その後の操作で利用できるようにします。2つ目の引数は「拒否」関数です。非同期操作が失敗した際にエラーメッセージを引数として呼び出します。
次に、データベースからユーザー情報を取得し、Promiseでユーザー情報を表示する前の例を書き直しましょう。
function getUserInfo(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
もし(typeof id === "number") {
const userInfo = {
id、
名前:「アダム・スミス」
年齢:30歳
};
resolve(userInfo);取得したユーザー情報で約束を解決する
} そうでなければ {
const error = new Error("Invalid ID")
reject(error) // エラー付きで約束を拒否する
}
}, 2000); 2秒の遅延をシミュレートします
})
}
function displayUserInfo(userInfo) {
console.log(ユーザー情報)
}
console.log(「ユーザー情報取得中...」);即時実行
getUserInfo(1)
.then((userInfo) => displayUserInfo(userInfo))
.catch((error) => console.log(error))
.finally(() => console.log(「完成」))
「getUserInfo」で最初に目立つ変更を加えたのは、元のコードを関数でラップし、関数で新しい「Promise」オブジェクトを構築し、その「Promise」オブジェクトを返すことです。さらに、コールバック関数を「resolve」関数に置き換え、これにより「getUserInfo」とコールバック関数を切り離すことができました。「resolve」関数を次のステップに提供できる引数を使っただけを考えればよくなります。特定のコールバック関数を呼び出すために必要な追加パラメータを考慮する必要がなくなりました。もう一つ目立つ変更点は、エラーが発生したときに直接エラーを投げるのではなく、'getUserInfo'の「reject」関数を呼び出すことです。
「getUserInfo」を呼び出す際には、もはやコールバック関数を渡す必要はありません。「getUserInfo」は「Promise」オブジェクトを返します。「Promise」オブジェクトの「then」メソッドを呼び出し、Promiseオブジェクトが正常に解決された後に呼び出したい関数を渡します。この関数は、getUserInfoの「resolve」関数と同じ引数を取ります。エラー処理については、「Promise」オブジェクトの「catch」メソッドを呼び出し、エラー処理の関数を「getUserInfo」の「reject」関数に渡します。最終的に、「finally」メソッドに渡された関数は、Promiseオブジェクトが決済(履行または拒否)されたときに実行されます。
「then」メソッドの強力な点は、メソッドチェーンと呼ばれるパターンで複数回呼び出せることです。「then」メソッドの呼び出しをチェインすることで、すべての操作が連続していることが強調され、コードがよりクリーンで読みやすくなります。以下はその例です。
function multiplyByTwo(input) {
return new Promise((resolve, reject) => {
もし(typeof input === "number") {
コンセント出力 = 入力 * 2
Resolve(出力)
} そうでなければ {
const error = 新しいエラー("Invalid Input")
reject(エラー)
}
})
}
multiplyByTwo(1)
.then((input) => multiplyByTwo(input))
.then((input) => multiplyByTwo(input))
.then((input) => multiplyByTwo(input))
.then((input) => multiplyByTwo(input))
.then((input) => console.log(input))
ご覧の通り、『then』メソッドを使って5回の「multiplyByTwo」呼び出しを連鎖しており、それぞれ(最初と最後のものを除く)は前の入力の出力を受け取り、その出力を次の入力に渡します。上記のコード例がコンソールに何回書き込んでいるか当ててみますか?
このブログでは、なぜ非同期プログラミングでPromiseを使う必要があるのか、そしてPromiseの使い方について説明しました。まず同期プログラミングと非同期プログラミングの違いを説明しました。次に、非同期プログラミングにおけるコールバック関数の使用とその避けられない欠点を実演しました。最後に、Promiseを使う利点といくつかの例を紹介しました。この記事がPromiseのなぜ、どのように使われるのかを理解する助けになれば幸いです。