あなたがトップ歌手で、ファンが日夜あなたの次の曲を求めていると想像してください。
少しでも安心してもらうために、出版したら送ると約束します。あなたはファンにリストを提供します。電子メール アドレスを入力すると、曲が利用可能になると、購読しているすべての当事者が即座にその曲を受け取ることができます。また、スタジオで火災が発生したなど、何か非常に問題が発生して曲が公開できなくなった場合でも、通知は届きます。
あなたはもう人が集まってこなくなるので、ファンはその曲を聴き逃すことがなくなるので、誰もが幸せです。
これは、プログラミングでよくあることを現実に例えたものです。
時間をかけて何かを行う「生成コード」。たとえば、ネットワーク経由でデータを読み込むコードなどです。それは「歌手」です。
「生成コード」の準備ができたら、その結果を求める「消費コード」。多くの関数がその結果を必要とする場合があります。それが「ファン」です。
Promiseは、「生成するコード」と「消費するコード」をリンクする特別な JavaScript オブジェクトです。たとえて言えば、これは「購読リスト」です。 「コードの生成」には、約束された結果を生成するために必要な時間がかかります。「約束」は、準備ができたときに、サブスクライブされたすべてのコードでその結果を利用できるようにします。
JavaScript の Promise は単純なサブスクリプション リストよりも複雑であり、追加の機能と制限があるため、このたとえはあまり正確ではありません。でも、最初は大丈夫です。
Promise オブジェクトのコンストラクター構文は次のとおりです。
letpromise = new Promise(function(resolve,拒否) { // エグゼキューター (生成コード、「シンガー」) });
new Promise
に渡される関数はexecutorと呼ばれます。 new Promise
が作成されると、executor が自動的に実行されます。これには、最終的に結果を生成する生成コードが含まれています。上の例えで言えば、執行者は「歌手」です。
その引数のresolve
とreject
JavaScript自体によって提供されるコールバックです。私たちのコードはエグゼキュータ内にのみあります。
エグゼキュータが結果を取得するときは、それが早いか遅いかは問題ではなく、次のコールバックのいずれかを呼び出す必要があります。
resolve(value)
— ジョブが正常に終了した場合、結果value
が返されます。
reject(error)
— エラーが発生した場合、 error
はエラー オブジェクトです。
要約すると、エグゼキュータは自動的に実行され、ジョブの実行を試みます。試行が終了すると、成功した場合はresolve
呼び出し、エラーがあった場合はreject
呼び出します。
new Promise
コンストラクターによって返されるpromise
オブジェクトには、次の内部プロパティがあります。
state
— 最初は"pending"
ですが、 resolve
が呼び出されたときに"fulfilled"
に、 reject
呼び出されたときに"rejected"
に変わります。
result
— 最初はundefined
、その後、 resolve(value)
が呼び出されたときにvalue
に変更され、 reject(error)
が呼び出されたときにerror
されます。
したがって、エグゼキュータは最終的にpromise
次のいずれかの状態に移動します。
後で、「ファン」がこれらの変更をどのように購読できるかを見ていきます。
以下は、時間のかかる ( setTimeout
経由で) コードを生成する Promise コンストラクターと単純なエグゼキューター関数の例です。
letpromise = new Promise(function(resolve,拒否) { // Promise が構築されると関数は自動的に実行されます // 1 秒後に「done」という結果でジョブが完了したことを知らせます setTimeout(() =>solve("完了"), 1000); });
上記のコードを実行すると、次の 2 つのことがわかります。
エグゼキューターは自動的かつ即座に ( new Promise
によって) 呼び出されます。
エグゼキュータは 2 つの引数、 resolve
とreject
受け取ります。これらの関数は JavaScript エンジンによって事前定義されているため、作成する必要はありません。準備ができたら、そのうちの 1 つだけを呼び出す必要があります。
1 秒間の「処理」の後、エグゼキュータは、 resolve("done")
を呼び出して結果を生成します。これにより、 promise
オブジェクトの状態が変更されます。
それは仕事が無事に完了した、つまり「約束が果たされた」という例でした。
次に、エグゼキュータがエラーで Promise を拒否する例を示します。
letpromise = new Promise(function(resolve,拒否) { // 1 秒後にジョブがエラーで終了したことを通知します setTimeout(() =>拒否(新しいエラー(「おっと!」)), 1000); });
request reject(...)
と、promise オブジェクトが"rejected"
状態に移行します。
要約すると、エグゼキューターはジョブ (通常は時間がかかるもの) を実行してから、 resolve
またはreject
呼び出して、対応する Promise オブジェクトの状態を変更する必要があります。
最初は「保留中」の Promise とは対照的に、解決または拒否された Promise は「解決済み」と呼ばれます。
結果は 1 つだけ、またはエラーが発生する可能性があります
エグゼキュータは、 resolve
またはreject
1 つだけ呼び出す必要があります。状態の変更はすべて最終的なものです。
それ以降のresolve
とreject
の呼び出しはすべて無視されます。
letpromise = new Promise(function(resolve,拒否) { 解決(「完了」); 拒否(新しいエラー("…")); // 無視される setTimeout(() =>solve("…")); // 無視される });
これは、実行者によって実行されたジョブの結果またはエラーが 1 つだけになる可能性があるという考えです。
また、 resolve
/ reject
1 つの引数のみ (または引数なし) を期待し、追加の引数は無視されます。
Error
オブジェクトを伴う拒否
何か問題が発生した場合には、実行者はreject
を呼び出す必要があります。これは、どのタイプの引数でも実行できます ( resolve
と同様)。ただし、 Error
オブジェクト (またはError
から継承したオブジェクト) を使用することをお勧めします。その理由はすぐに明らかになるでしょう。
すぐにresolve
/ reject
呼び出します
実際には、エグゼキュータは通常、何かを非同期的に実行し、しばらくしてからresolve
/ reject
呼び出しますが、そうする必要はありません。次のように、すぐにresolve
またはreject
呼び出すこともできます。
letpromise = new Promise(function(resolve,拒否) { // 仕事に時間をかけない 解決(123); // すぐに結果が返されます: 123 });
たとえば、ジョブの実行を開始した後、すべてがすでに完了してキャッシュされていることがわかった場合に、これが発生する可能性があります。
それはいいです。すぐに約束が決まりました。
state
とresult
内部のものです
Promise オブジェクトのプロパティのstate
とresult
は内部的なものです。直接アクセスすることはできません。そのためには、 .then
/ .catch
/ .finally
メソッドを使用できます。それらについては以下で説明します。
Promise オブジェクトは、実行者 (「生成コード」または「シンガー」) と結果またはエラーを受け取る消費関数 (「ファン」) の間のリンクとして機能します。使用する関数は、 .then
および.catch
メソッドを使用して登録 (サブスクライブ) できます。
最も重要で基本的なものは.then
です。
構文は次のとおりです。
約束します。それでは( function(result) { /* 成功した結果を処理します */ }, function(error) { /* エラーを処理します */ } );
.then
の最初の引数は、Promise が解決され、結果を受け取るときに実行される関数です。
.then
の第 2 引数は、Promise が拒否されてエラーを受け取ったときに実行される関数です。
たとえば、正常に解決された Promise に対する反応は次のとおりです。
letpromise = new Promise(function(resolve,拒否) { setTimeout(() =>solve("完了!"), 1000); }); //solve は .then の最初の関数を実行します 約束します。それでは( result =>alert(result), // 「完了!」を表示します。 1秒後 error =>alert(error) // 実行されません );
最初の関数が実行されました。
拒否の場合、2 番目は次のようになります。
letpromise = new Promise(function(resolve,拒否) { setTimeout(() =>拒否(新しいエラー(「おっと!」)), 1000); }); //拒否は .then の 2 番目の関数を実行します 約束します。それでは( result =>alert(result), // 実行されません error =>alert(error) // 「エラー: おっと!」を表示します。 1秒後 );
正常な完了のみに興味がある場合は、 .then
に関数の引数を 1 つだけ指定できます。
let replace = new Promise(resolve => { setTimeout(() =>solve("完了!"), 1000); }); promise.then(アラート); // 「完了!」を表示します。 1秒後
エラーのみに興味がある場合は、最初の引数としてnull
使用できます: .then(null, errorHandlingFunction)
。または、まったく同じ.catch(errorHandlingFunction)
を使用することもできます。
letpromise = new Promise((解決、拒否) => { setTimeout(() =>拒否(新しいエラー(「おっと!」)), 1000); }); // .catch(f) はpromise.then(null, f) と同じです プロミス.キャッチ(アラート); // 「エラー: おっと!」を表示します。 1秒後
.catch(f)
呼び出しは.then(null, f)
の完全な類似物であり、単なる省略表現です。
通常のtry {...} catch {...}
にfinally
句があるのと同じように、promiseにもfinally
があります。
呼び出し.finally(f)
promise が解決されるとき (解決するか拒否するか) に常にf
実行されるという意味で.then(f, f)
に似ています。
finally
の考え方は、前の操作が完了した後にクリーンアップ/ファイナライズを実行するためのハンドラーをセットアップすることです。
たとえば、読み込みインジケーターの停止、不要になった接続の終了などです。
パーティーのフィニッシャーとして考えてください。パーティーが良かったのか悪かったのか、参加した友人の数に関係なく、パーティーの後は片づけが必要です(少なくともそうする必要があります)。
コードは次のようになります。
new Promise((解決、拒否) => { /* 時間がかかることを実行してから、resolve を呼び出すか、場合によっては拒否を呼び出します */ }) // Promise が解決されたときに実行されます。成功したかどうかは関係ありません .finally(() => 読み込み停止インジケーター) // したがって、次に進む前に読み込みインジケーターは常に停止します .then(result => 結果を表示、err => エラーを表示)
ただし、 finally(f)
正確にはthen(f,f)
のエイリアスではないことに注意してください。
重要な違いがあります:
finally
ハンドラーには引数がありません。 finally
約束が成功したかどうかはわかりません。通常、私たちの仕事は「一般的な」最終処理手順を実行することなので、それで問題ありません。
上の例を見てください。ご覧のとおり、 finally
ハンドラーには引数がなく、promise の結果は next ハンドラーによって処理されます。
finally
ハンドラーは、結果またはエラーを次の適切なハンドラーに「パススルー」します。
たとえば、ここでは結果がfinally
then
に渡されます。
new Promise((解決、拒否) => { setTimeout(() =>solve("値"), 2000); }) .finally(() =>alert("Promise Ready")) // 最初にトリガーします .then(結果 => アラート(結果)); // <-- .then は「値」を表示します
ご覧のとおり、最初の Promise によって返されたvalue
finally
次のthen
に渡されます。
finally
Promise の結果を処理するものではないため、これは非常に便利です。前述したように、結果がどうであれ、ここは一般的なクリーンアップを行う場所です。
そして、これがエラーの例であり、 finally
どのように渡されてcatch
されるのかを確認するためのものです。
new Promise((解決、拒否) => { throw new Error("error"); }) .finally(() =>alert("Promise Ready")) // 最初にトリガーします .catch(err => アラート(err)); // <-- .catch はエラーを表示します
finally
ハンドラーも何も返してはなりません。存在する場合、戻り値は暗黙的に無視されます。
このルールの唯一の例外は、 finally
ハンドラーがエラーをスローする場合です。その後、このエラーは前の結果ではなく、次のハンドラーに送られます。
要約すると:
finally
ハンドラーは、前のハンドラーの結果を取得しません (引数がありません)。この結果は、代わりに次の適切なハンドラーに渡されます。
finally
ハンドラーが何かを返した場合、それは無視されます。
finally
エラーがスローされると、実行は最も近いエラー ハンドラーに移ります。
これらの機能は便利で、一般的なクリーンアップ手順など、 finally
本来の使用方法で使用すると、物事が適切に機能します。
解決済みの Promise にハンドラーをアタッチできます
Promise が保留中の場合、 .then/catch/finally
ハンドラーはその結果を待ちます。
場合によっては、Promise にハンドラーを追加したときに、Promise がすでに解決されている可能性があります。
そのような場合、これらのハンドラーはただちに実行されます。
// Promise は作成後すぐに解決されます letpromise = new Promise(resolve =>solve("done!")); promise.then(アラート); // 終わり! (今すぐ現れます)
これにより、Promise が現実の「サブスクリプション リスト」シナリオよりも強力になることに注意してください。歌手がすでに曲をリリースしていて、サブスクリプション リストに登録した人は、おそらくその曲を受信しないでしょう。実際の購読はイベント前に行う必要があります。
Promise はより柔軟になります。ハンドラーはいつでも追加できます。結果がすでに存在する場合は、そのまま実行されます。
次に、Promise が非同期コードの作成にどのように役立つかについて、より実践的な例を見てみましょう。
前の章でスクリプトをロードするためのloadScript
関数を用意しました。
コールバックベースのバリアントをここで思い出してください。
関数loadScript(src, callback) { let script = document.createElement('script'); スクリプト.src = ソース; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`${src}` のスクリプト読み込みエラー)); document.head.append(スクリプト); }
Promiseを使って書き換えてみましょう。
新しい関数loadScript
コールバックは必要ありません。代わりに、読み込みが完了したときに解決される Promise オブジェクトを作成して返します。外側のコードは、 .then
を使用してハンドラー (サブスクライブ関数) を追加できます。
関数loadScript(src) { return new Promise(function(resolve,拒否) { let script = document.createElement('script'); スクリプト.src = ソース; script.onload = () => 解決(スクリプト); script.onerror = () => accept(new Error(`${src}` のスクリプト読み込みエラー)); document.head.append(スクリプト); }); }
使用法:
letpromise =loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); 約束します。それでは( script =>alert(`${script.src} がロードされました!`)、 エラー => アラート(`エラー: ${error.message}`) ); promise.then(script =>alert('別のハンドラー...'));
コールバックベースのパターンに比べて、いくつかの利点がすぐにわかります。
約束 | コールバック |
---|---|
約束によって、私たちは自然な順序で物事を行うことができます。まず、 loadScript(script) を実行し、 .then 結果をどうするかを記述します。 | loadScript(script, callback) を呼び出すときは、自由に使えるcallback 関数が必要です。言い換えれば、 loadScript が呼び出される前に、結果をどう処理するかを知っておく必要があります。 |
Promise で.then 何度でも呼び出すことができます。そのたびに、新しい「ファン」、新しい購読機能を「購読リスト」に追加していきます。これについては、次の章「Promise Chaining」で詳しく説明します。 | コールバックは 1 つだけ存在できます。 |
そのため、Promise はコード フローと柔軟性を向上させます。しかし、それだけではありません。それについては次の章で見ていきます。
以下のコードの出力は何でしょうか?
letpromise = new Promise(function(resolve,拒否) { 解決(1); setTimeout(() => 解決(2), 1000); }); promise.then(アラート);
出力は次のようになります: 1
。
最初のreject/resolve
の呼び出しのみが考慮されるため、2 番目のresolve
の呼び出しは無視されます。それ以降の呼び出しは無視されます。
組み込み関数setTimeout
コールバックを使用します。約束に基づいた代替案を作成します。
関数delay(ms)
は Promise を返す必要があります。その Promise はms
ミリ秒後に解決されるはずなので、次のように.then
を追加できます。
関数遅延(ミリ秒) { // あなたのコード } 遅延(3000).then(() => アラート('3 秒後に実行'));
関数遅延(ミリ秒) { return new Promise(resolve => setTimeout(resolve, ms)); } 遅延(3000).then(() => アラート('3 秒後に実行'));
このタスクでは、引数なしでresolve
が呼び出されることに注意してください。 delay
から値を返すことはなく、遅延を確保するだけです。
コールバックを受け入れる代わりに Promise を返すように、タスク アニメーション円のソリューションにあるshowCircle
関数をコールバックで書き換えます。
新しい使い方:
showCircle(150, 150, 100).then(div => { div.classList.add('メッセージボール'); div.append("こんにちは、世界!"); });
タスクの解決策として、コールバックをベースとしたアニメーション円を使用します。
サンドボックスでソリューションを開きます。