「はじめに」の章で述べた問題に戻りましょう: コールバック: スクリプトの読み込みなど、一連の非同期タスクを次々に実行する必要があります。どうすればうまくコーディングできるでしょうか?
Promise では、そのためのレシピをいくつか提供しています。
この章では、プロミスチェーンについて説明します。
次のようになります。
new Promise(function(解決、拒否) { setTimeout(() => 解決(1), 1000); // (*) }).then(function(result) { // (**) アラート(結果); // 1 結果を返します * 2; }).then(function(result) { // (***) アラート(結果); // 2 結果を返します * 2; }).then(関数(結果) { アラート(結果); // 4 結果を返します * 2; });
その考え方は、結果が.then
ハンドラーのチェーンを介して渡されるということです。
ここでの流れは次のとおりです。
最初の約束は 1 秒で解決されます(*)
、
次に、 .then
ハンドラーが呼び出され(**)
、これにより新しい Promise (値2
で解決される) が作成されます。
次のthen
(***)
前の結果を取得し、それを処理 (倍増) して次のハンドラーに渡します。
…等々。
結果がハンドラーのチェーンに沿って渡されると、一連のalert
呼び出し ( 1
→ 2
→ 4
が確認できます。
.then
を呼び出すたびに新しい Promise が返されるため、すべてが機能し、それに対して次の.then
を呼び出すことができます。
ハンドラーが値を返すと、それはその Promise の結果になるため、次の.then
がそれを使って呼び出されます。
古典的な初心者の間違い: 技術的には、1 つの Promise に多数の.then
を追加することもできます。これは連鎖ではありません。
例えば:
letpromise = new Promise(function(resolve,拒否) { setTimeout(() => 解決(1), 1000); }); promise.then(関数(結果) { アラート(結果); // 1 結果を返します * 2; }); promise.then(関数(結果) { アラート(結果); // 1 結果を返します * 2; }); promise.then(関数(結果) { アラート(結果); // 1 結果を返します * 2; });
ここで行ったのは、1 つの Promise に複数のハンドラーを追加しただけです。彼らは結果を互いに渡しません。代わりに、独立して処理します。
これが図です (上記のチェーンと比較してください)。
同じ Promise 上のすべての.then
同じ結果、つまりその Promise の結果を取得します。したがって、上記のコードでは、すべてのalert
同じように表示されます1
実際には、1 つの Promise に対して複数のハンドラーが必要になることはほとんどありません。チェーンはより頻繁に使用されます。
.then(handler)
で使用されるハンドラーは、Promise を作成して返すことができます。
その場合、さらなるハンドラーは、問題が解決するまで待機し、結果を取得します。
例えば:
new Promise(function(解決、拒否) { setTimeout(() => 解決(1), 1000); }).then(関数(結果) { アラート(結果); // 1 return new Promise((解決、拒否) => { // (*) setTimeout(() => 解決(結果 * 2), 1000); }); }).then(function(result) { // (**) アラート(結果); // 2 return new Promise((解決、拒否) => { setTimeout(() => 解決(結果 * 2), 1000); }); }).then(関数(結果) { アラート(結果); // 4 });
ここで、最初の.then
1
表示し、行(*)
でnew Promise(…)
を返します。 1 秒後に解決され、結果 ( resolve
の引数、ここではresult * 2
) が 2 番目の.then
のハンドラーに渡されます。そのハンドラーは(**)
行にあり、 2
と表示され、同じことを行います。
したがって、出力は前の例と同じ 1 → 2 → 4 になりますが、 alert
呼び出しの間に 1 秒の遅延が生じます。
Promise を返すことで、非同期アクションのチェーンを構築できます。
この機能を前の章で定義したloadScript
とともに使用して、スクリプトを 1 つずつ順番にロードしてみましょう。
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(関数(スクリプト) { returnloadScript("https://javascript.info/article/promise-chaining/two.js"); }) .then(関数(スクリプト) { returnloadScript("https://javascript.info/article/promise-chaining/three.js"); }) .then(関数(スクリプト) { // スクリプト内で宣言された関数を使用する // 実際にロードされたことを示すため 1つ(); 二(); 三つ(); });
このコードは、アロー関数を使用して少し短くすることができます。
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(script =>loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script =>loadScript("https://javascript.info/article/promise-chaining/three.js")) .then(スクリプト => { // スクリプトがロードされるので、そこで宣言された関数を使用できます 1つ(); 二(); 三つ(); });
ここで、各loadScript
呼び出しはPromiseを返し、それが解決されると次の.then
が実行されます。次に、次のスクリプトの読み込みを開始します。したがって、スクリプトが次々に読み込まれます。
チェーンにさらに非同期アクションを追加できます。コードはまだ「フラット」であることに注意してください。コードは右ではなく下に伸びています。 「破滅のピラミッド」の兆候はありません。
技術的には、次のように.then
各loadScript
に直接追加できます。
loadScript("https://javascript.info/article/promise-chaining/one.js").then(script1 => { loadScript("https://javascript.info/article/promise-chaining/two.js").then(script2 => { loadScript("https://javascript.info/article/promise-chaining/three.js").then(script3 => { // この関数は変数 script1、script2、script3 にアクセスできます。 1つ(); 二(); 三つ(); }); }); });
このコードも同じことを行い、3 つのスクリプトを順番に読み込みます。しかし、それは「右に伸びる」のです。したがって、コールバックの場合と同じ問題が発生します。
Promise を使い始める人は、連鎖について知らない場合があるため、このように記述します。一般に、連鎖することが好ましい。
入れ子関数は外側のスコープにアクセスできるため、 .then
を直接記述しても問題ない場合があります。上記の例では、最もネストされたコールバックは、すべての変数script1
、 script2
、 script3
にアクセスできます。しかし、それは規則ではなく例外です。
センナブル
正確に言うと、ハンドラーは正確には Promise ではなく、いわゆる「thenable」オブジェクト、つまり.then
メソッドを持つ任意のオブジェクトを返す場合があります。これは約束と同じように扱われます。
この考えは、サードパーティのライブラリが独自の「promise 互換性のある」オブジェクトを実装できるということです。これらはメソッドの拡張セットを持つことができますが、 .then
を実装しているため、ネイティブの Promise との互換性もあります。
thenable オブジェクトの例を次に示します。
クラス thenable { コンストラクター(番号) { this.num = 数値; } then(解決、拒否) { アラート(解決); // function() { ネイティブコード } // 1 秒後に this.num*2 で解決します setTimeout(() =>solve(this.num * 2), 1000); // (**) } } 新しい約束(解決 => 解決(1)) .then(結果 => { 新しい thenable(結果) を返します。 // (*) }) .then(アラート); // 1000ms 後に 2 を表示
JavaScript は、行(*)
の.then
ハンドラーによって返されたオブジェクトをチェックします。 then
という名前の呼び出し可能なメソッドがある場合は、そのメソッドを呼び出して、ネイティブ関数resolve
を提供するメソッドを呼び出し、引数として (エグゼキューターと同様に) reject
、それらのいずれかが見つかるまで待機します。と呼ばれます。上記の例では、 resolve(2)
は 1 秒後に呼び出されます(**)
。次に、結果がチェーンのさらに下に渡されます。
この機能を使用すると、 Promise
から継承することなく、カスタム オブジェクトを Promise チェーンと統合できます。
フロントエンド プログラミングでは、Promise はネットワーク リクエストによく使用されます。それでは、その拡張例を見てみましょう。
fetch メソッドを使用して、リモート サーバーからユーザーに関する情報を読み込みます。多くのオプションのパラメータが別の章で説明されていますが、基本的な構文は非常に単純です。
letpromise = fetch(url);
これにより、 url
に対してネットワーク リクエストが行われ、Promise が返されます。 Promise は、リモート サーバーがヘッダーで応答するとき、完全な応答がダウンロードされる前に、 response
オブジェクトで解決されます。
完全な応答を読み取るには、メソッドresponse.text()
を呼び出す必要があります。このメソッドは、リモート サーバーから全文がダウンロードされたときに解決される Promise を返し、結果としてそのテキストが返されます。
以下のコードはuser.json
にリクエストを作成し、そのテキストをサーバーから読み込みます。
fetch('https://javascript.info/article/promise-chaining/user.json') // .then リモートサーバーが応答すると以下が実行されます .then(関数(応答) { // response.text() は完全な応答テキストで解決される新しい Promise を返します // ロード時 応答.text()を返します; }) .then(関数(テキスト) { // ...リモート ファイルの内容は次のとおりです アラート(テキスト); // {"name": "iliakan", "isAdmin": true} });
fetch
から返されるresponse
オブジェクトには、リモート データを読み取り、JSON として解析するメソッドresponse.json()
も含まれています。私たちの場合、それはさらに便利なので、それに切り替えてみましょう。
簡潔にするためにアロー関数も使用します。
// 上記と同じですが、response.json() はリモート コンテンツを JSON として解析します fetch('https://javascript.info/article/promise-chaining/user.json') .then(応答 => 応答.json()) .then(ユーザー => アラート(ユーザー名)); // iliakan、ユーザー名を取得
次に、ロードされたユーザーで何かをしてみましょう。
たとえば、GitHub にもう 1 つのリクエストを送信し、ユーザー プロファイルをロードしてアバターを表示できます。
// user.json をリクエストする fetch('https://javascript.info/article/promise-chaining/user.json') // jsonとしてロードします .then(応答 => 応答.json()) // GitHub にリクエストを送信する .then(user => fetch(`https://api.github.com/users/${user.name}`)) // レスポンスを json としてロードします .then(応答 => 応答.json()) // アバター画像 (githubUser.avatar_url) を 3 秒間表示します (アニメーション化する可能性があります) .then(githubUser => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "約束のアバターの例"; document.body.append(img); setTimeout(() => img.remove(), 3000); // (*) });
コードは機能します。詳細についてはコメントを参照してください。ただし、そこには潜在的な問題があり、Promise を使い始める人にとって典型的なエラーです。
行(*)
を見てください: アバターの表示が終了して削除された後、どうすればよいでしょうか?たとえば、そのユーザーまたは他のものを編集するためのフォームを表示したいとします。今のところ、方法はありません。
チェーンを拡張可能にするには、アバターの表示が終了したときに解決される Promise を返す必要があります。
このような:
fetch('https://javascript.info/article/promise-chaining/user.json') .then(応答 => 応答.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(応答 => 応答.json()) .then(githubUser => new Promise(function(resolve, accept) { // (*) let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "約束のアバターの例"; document.body.append(img); setTimeout(() => { img.remove(); 解決(githubUser); // (**) }, 3000); })) // 3秒後にトリガー .then(githubUser =>alert(`${githubUser.name}`の表示を終了しました`));
つまり、行(*)
の.then
ハンドラーはnew Promise
を返すようになり、 setTimeout
(**)
でのresolve(githubUser)
の呼び出し後にのみ解決されます。チェーン内の次の.then
それを待ちます。
良い習慣として、非同期アクションは常に Promise を返す必要があります。これにより、その後のアクションを計画することが可能になります。今はチェーンを拡張する予定がなくても、後で必要になる可能性があります。
最後に、コードを再利用可能な関数に分割できます。
関数loadJson(url) { 取得(url)を返す .then(応答 => 応答.json()); } 関数loadGithubUser(名前) { returnloadJson(`https://api.github.com/users/${name}`); } 関数 showAvatar(githubUser) { return new Promise(function(resolve,拒否) { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "約束のアバターの例"; document.body.append(img); setTimeout(() => { img.remove(); 解決(githubUser); }, 3000); }); } // それらを使用します: loadJson('https://javascript.info/article/promise-chaining/user.json') .then(user =>loadGithubUser(user.name)) .then(アバターを表示) .then(githubUser =>alert(`${githubUser.name}`の表示を終了しました`)); // ...
.then
(またはcatch/finally
関係ありません) ハンドラーが Promise を返した場合、チェーンの残りの部分はそれが解決するまで待機します。実行されると、その結果 (またはエラー) がさらに渡されます。
全体像は次のとおりです。
これらのコード断片は等しいでしょうか?言い換えれば、どのハンドラー関数でも、どのような状況でも同じように動作するのでしょうか?
プロミス.then(f1).catch(f2);
対:
約束.then(f1, f2);
簡単に言うと、 「いいえ、それらは等しくありません」です。
違いは、 f1
でエラーが発生した場合、それが.catch
によって処理されることです。
約束 .then(f1) .catch(f2);
…しかし、ここではそうではありません:
約束 .then(f1, f2);
これは、エラーがチェーンに渡され、2 番目のコード部分にはf1
より下にチェーンがないためです。
言い換えれば、 .then
結果/エラーを次の.then/catch
に渡します。したがって、最初の例には以下のcatch
がありますが、2 番目の例にはキャッチがないため、エラーは処理されません。