ここの例ではブラウザメソッドを使用します
コールバック、プロミス、その他の抽象的な概念の使用を示すために、いくつかのブラウザー メソッド、具体的にはスクリプトの読み込みと単純なドキュメント操作の実行を使用します。
これらのメソッドに慣れておらず、例での使用法がわかりにくい場合は、チュートリアルの次の部分からいくつかの章を読むとよいでしょう。
ただし、とにかく物事を明確にしようとします。ブラウザに関しては特に複雑なことは何もありません。
非同期アクションをスケジュールできるようにする多くの機能が JavaScript ホスト環境によって提供されます。言い換えれば、今すぐに開始しても、完了するのは後でということです。
たとえば、そのような関数の 1 つはsetTimeout
関数です。
非同期アクションの実際の例は他にもあります。たとえば、スクリプトやモジュールのロードなどです (後の章で説明します)。
指定されたsrc
でスクリプトをロードする関数loadScript(src)
を見てください。
関数loadScript(src) { // <script> タグを作成し、ページに追加します // これにより、指定された src を持つスクリプトが読み込みを開始し、完了すると実行されます。 let script = document.createElement('script'); スクリプト.src = ソース; document.head.append(スクリプト); }
これは、指定されたsrc
を持つ動的に作成された新しいタグ<script src="…">
をドキュメントに挿入します。ブラウザは自動的にロードを開始し、完了すると実行します。
この関数は次のように使用できます。
// 指定されたパスでスクリプトをロードして実行します loadScript('/my/script.js');
スクリプトは今ロードを開始するため「非同期」に実行されますが、関数がすでに終了した後で実行されます。
loadScript(…)
の下にコードがある場合、スクリプトの読み込みが完了するまで待機しません。
loadScript('/my/script.js'); // 以下のコードはloadScript // スクリプトの読み込みが完了するのを待ちません // ...
新しいスクリプトが読み込まれたらすぐに使用する必要があるとします。新しい関数を宣言しており、それらを実行したいと考えています。
しかし、 loadScript(…)
呼び出しの直後にこれを実行すると、機能しません。
loadScript('/my/script.js'); // スクリプトには「function newFunction() {…}」が含まれています newFunction(); // そのような関数はありません!
当然のことながら、ブラウザにはスクリプトを読み込む時間がなかったと考えられます。現時点では、 loadScript
関数にはロードの完了を追跡する方法がありません。スクリプトがロードされ、最終的に実行されるだけです。しかし、そのスクリプトから新しい関数と変数を使用するために、それがいつ起こるかを知りたいと考えています。
スクリプトの読み込み時に実行するcallback
関数を、 loadScript
の 2 番目の引数として追加しましょう。
関数loadScript(src, callback) { let script = document.createElement('script'); スクリプト.src = ソース; script.onload = () => コールバック(スクリプト); document.head.append(スクリプト); }
onload
イベントについては、「リソースの読み込み: onload と onerror」の記事で説明されています。基本的に、スクリプトが読み込まれて実行された後に関数が実行されます。
ここで、スクリプトから新しい関数を呼び出したい場合は、それをコールバックに記述する必要があります。
loadScript('/my/script.js', function() { // スクリプトがロードされた後にコールバックが実行されます newFunction(); // これで動作するようになりました ... });
これが考え方です。2 番目の引数は、アクションが完了したときに実行される関数 (通常は匿名) です。
実際のスクリプトを使用した実行可能な例を次に示します。
関数loadScript(src, callback) { let script = document.createElement('script'); スクリプト.src = ソース; script.onload = () => コールバック(スクリプト); document.head.append(スクリプト); } loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { alert(`クール、スクリプト ${script.src} がロードされました`); アラート( _ ); // _ はロードされたスクリプト内で宣言された関数です });
これは、「コールバックベース」スタイルの非同期プログラミングと呼ばれます。非同期で何かを実行する関数は、完了後に関数を実行するcallback
引数を提供する必要があります。
ここではloadScript
で実行しましたが、もちろんこれは一般的なアプローチです。
2 つのスクリプトを順番にロードするにはどうすればよいでしょうか。最初のスクリプトと、その後に 2 番目のスクリプトをロードするにはどうすればよいでしょうか?
自然な解決策は、次のように 2 番目のloadScript
呼び出しをコールバック内に置くことです。
loadScript('/my/script.js', function(script) { alert(`すごいですね、${script.src} がロードされました。もう 1 つロードしましょう`); loadScript('/my/script2.js', function(script) { alert(`クール、2 番目のスクリプトが読み込まれました`); }); });
外側のloadScript
が完了すると、コールバックによって内側のloadScriptが開始されます。
もう 1 つのスクリプトが必要な場合はどうすればよいでしょうか?
loadScript('/my/script.js', function(script) { loadScript('/my/script2.js', function(script) { loadScript('/my/script3.js', function(script) { // ...すべてのスクリプトがロードされた後に続行します }); }); });
したがって、すべての新しいアクションはコールバック内にあります。一部のアクションにはこれで問題ありませんが、多くのアクションには適さないため、他のバリエーションもすぐに確認する予定です。
上記の例では、エラーは考慮されていませんでした。スクリプトの読み込みに失敗した場合はどうなりますか?コールバックはそれに反応できるはずです。
以下は、読み込みエラーを追跡する、 loadScript
の改良版です。
関数loadScript(src, callback) { let script = document.createElement('script'); スクリプト.src = ソース; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`${src}` のスクリプト読み込みエラー)); document.head.append(スクリプト); }
ロードが成功した場合はcallback(null, script)
を呼び出し、それ以外の場合はcallback(error)
を呼び出します。
使用法:
loadScript('/my/script.js', function(error, script) { if (エラー) { // エラーを処理します } それ以外 { // スクリプトが正常にロードされました } });
もう一度言いますが、 loadScript
に使用したレシピは、実際には非常に一般的なものです。これは「error-first コールバック」スタイルと呼ばれます。
規則は次のとおりです。
callback
の最初の引数は、エラーが発生した場合に備えて予約されています。次に、 callback(err)
が呼び出されます。
2 番目の引数 (必要に応じて次の引数) は、成功した結果を表します。次に、 callback(null, result1, result2…)
が呼び出されます。
したがって、単一のcallback
関数は、エラーの報告と結果の返送の両方に使用されます。
一見すると、これは非同期コーディングへの実行可能なアプローチのように見えます。そして実際その通りです。 1 つまたはおそらく 2 つのネストされた呼び出しについては、問題ないようです。
ただし、複数の非同期アクションが次々に続く場合は、次のようなコードになります。
loadScript('1.js', function(error, script) { if (エラー) { ハンドルエラー(エラー); } それ以外 { // ... loadScript('2.js', function(error, script) { if (エラー) { ハンドルエラー(エラー); } それ以外 { // ... loadScript('3.js', function(error, script) { if (エラー) { ハンドルエラー(エラー); } それ以外 { // ...すべてのスクリプトがロードされた後に続行します (*) } }); } }); } });
上記のコードでは次のようになります。
1.js
をロードし、エラーがなければ…
2.js
をロードします。エラーがなければ…
3.js
をロードし、エラーがなければ、別のことを実行します(*)
。
呼び出しがよりネストされると、コードはより深くなり、管理がますます難しくなります。特に、 ...
の代わりに実際のコードがある場合、より多くのループや条件文などが含まれる可能性があります。
それは「コールバック地獄」または「破滅のピラミッド」と呼ばれることもあります。
入れ子になった呼び出しの「ピラミッド」は、非同期アクションが発生するたびに右に成長します。すぐにそれは制御不能になります。
したがって、このコーディング方法はあまり良くありません。
次のように、すべてのアクションをスタンドアロン関数にすることで問題を軽減できます。
loadScript('1.js', ステップ 1); 関数ステップ1(エラー、スクリプト) { if (エラー) { ハンドルエラー(エラー); } それ以外 { // ... loadScript('2.js', ステップ 2); } } 関数ステップ 2(エラー、スクリプト) { if (エラー) { ハンドルエラー(エラー); } それ以外 { // ... loadScript('3.js', ステップ 3); } } 関数ステップ 3(エラー、スクリプト) { if (エラー) { ハンドルエラー(エラー); } それ以外 { // ...すべてのスクリプトがロードされた後に続行します (*) } }
見る?これも同じことを行います。すべてのアクションを個別のトップレベル関数にしたため、深いネストはありません。
機能しますが、コードは引き裂かれたスプレッドシートのように見えます。読むのが難しく、読んでいる間、部分間を目で移動する必要があることにおそらくお気づきでしょう。これは、特に読者がコードに詳しくなく、どこに目をジャンプすればよいかわからない場合には不便です。
また、 step*
という名前の関数はすべて 1 回限りの使用であり、「破滅のピラミッド」を回避するためだけに作成されています。アクションチェーンの外でそれらを再利用する人は誰もいません。したがって、ここでは名前空間が少し乱雑になっています。
もっと良いものを作りたいと思っています。
幸いなことに、そのようなピラミッドを回避する他の方法があります。最良の方法の 1 つは、次の章で説明する「約束」を使用することです。