JSの仕組み:イベントループ、非同期、およびasync / awaitを使用してコードを改善する5つの方法

[アドバイスを読む]サイクルの他の19の部分
パート1: エンジン、ランタイムメカニズム、コールスタックの概要

パート2: V8内部とコードの最適化について

パート3: メモリ管理、4種類のメモリリーク、およびそれらとの戦い

パート4: イベントループ、非同期、および非同期/待機を使用してコードを改善する5つの方法

パート5: WebSocketとHTTP / 2 + SSE。 何を選ぶ?

パート6: WebAssemblyの機能と範囲

パート7: Web Workersと5つの使用シナリオ

パート8: サービスワーカー

パート9: Webプッシュ通知

パート10: MutationObserverを使用してDOMの変更を追跡する

パート11: Webページレンダリングエンジンとパフォーマンスを最適化するためのヒント

パート12: パフォーマンスとセキュリティを最適化するブラウザーネットワークサブシステム

パート12: パフォーマンスとセキュリティを最適化するブラウザーネットワークサブシステム

パート13: CSSとJavaScriptを使用したアニメーション

パート14: JSの仕組み:抽象構文ツリー、解析、およびその最適化

パート15: JSの仕組み:クラスと継承、BabelおよびTypeScriptでのトランスピレーション

パート16: JSの仕組み:ストレージ

パート17: JSの仕組み:Shadow DOMテクノロジーとWebコンポーネント

パート18: JSの仕組み:WebRTCおよびP2P通信メカニズム

パート19: JSの仕組み:カスタム要素


JavaScriptの内部機能に特化した一連の資料の4番目の部分を次に示します。 これらの資料は、一方ではJS言語およびエコシステムの基本要素を研究することを目的としており、もう一方ではSessionStackでのソフトウェア開発の実践に基づいた推奨事項を含んでいます。 競争力のあるJSアプリケーションは、高速で信頼性が高くなければなりません。 このようなアプリケーションの作成は、最終的に、JavaScriptメカニズムに興味がある人が達成しようとする目標です。



画像



この記事は、 最初の資料の開発とみなすことができます。 ここでは、シングルスレッド実行モデルの制限を検討し、これらの制限を克服する方法について説明します。 たとえば、JSで高品質のユーザーインターフェイスを開発する場合。 いつものように、材料の最後に、考慮された技術の使用に関する実用的な推奨事項が与えられます。 この記事の主なトピックは非同期開発なので、これらはasync / await



を使用してコードを改善するための5つのヒントです。



シングルスレッドコード実行モデルの制限



最初の記事では、「コールスタックに完了までに長い時間がかかる関数があるとどうなりますか?」という質問について考えました。 これらの考察を続けます。



ブラウザで実行される複雑な画像処理アルゴリズムを想像してください。 呼び出しスタックに機能する関数がある場合、ブラウザは他に何もできません。 彼はブロックされています。 つまり、ブラウザは画面に何も表示できず、他のコードを実行できません。 この状況の最も顕著な結果は、ユーザーインターフェイスの「ブレーキ」です。 Webページは単に「ジャミング」されています。



場合によっては、これはそれほど深刻な問題ではないかもしれません。 ただし、これは最悪ではありません。 ブラウザがあまりにも多くのタスクを実行し始めるとすぐに、ユーザーの影響に長時間応答しない場合があります。 通常、このような状況では、ブラウザーは保護対策を講じてエラーメッセージを表示し、問題のページを閉じるかどうかをユーザーに尋ねます。 ユーザーがそのようなメッセージを見ない方が良いでしょう。 それらは、美しく便利なインターフェースを作成する開発者のすべての努力を完全に台無しにします。









しかし、Webアプリケーションの外観を美しくし、複雑な計算を実行できるようにするにはどうすればよいでしょうか? JSアプリケーションの構成要素を分析することにより、この質問に対する答えを探し始めます。



JavaScriptの構成要素



JavaScriptアプリケーションコードは単一の.jsファイルに配置できますが、ほぼ確実に複数のブロックで構成されます。 この場合、これらのブロックのうちの1つだけが特定の時点で実行されます。 残りは後で実行されます。 JavaScriptの最も一般的なコードブロックは関数です。



どうやら、ほとんどの新しいJS開発者は、「後で」が「すぐに」すぐに続くとは限らないという事実を完全には認識していません。 つまり、「今」完了できないタスクは非同期で実行する必要があります。 これは、おそらく知らないうちに、あなたが予期していたはずのブロック動作に陥らないことを意味します。



次の例を見てください。



 // ajax(..) -   Ajax- var response = ajax('https://example.com/api'); console.log(response); //   response     api
      
      





標準のAjaxリクエストが同期的に実行されないことをご存知かもしれません。 つまり、呼び出し直後のajax(..)



関数は、 response



変数に割り当てることができる値を返すことができません。



非同期関数によって返される結果の「期待」を整理するための単純なメカニズムは、いわゆるコールバック関数、またはコールバックを使用することです。



 ajax('https://example.com/api', function(response) {   console.log(response); //   response   api });
      
      





ここで、Ajaxリクエストを同期的に実行できることに注意してください。 ただし、これは行わないでください。 同期Ajaxリクエストを実行すると、JSアプリケーションのユーザーインターフェイスがブロックされます。 ユーザーはボタンをクリックしたり、フィールドにデータを入力したり、ページをスクロールすることさえできなくなります。 Ajaxリクエストの同期実行により、ユーザーはアプリケーションと対話できなくなります。 このアプローチは、可能ですが、悲惨な結果につながります。



これがこの恐怖の様子です。ただし、このようなことは絶対に書かないでください。Webを通常の動作が不可能な場所に変えないでください。



 //   ,    jQuery jQuery.ajax({   url: 'https://api.example.com/endpoint',   success: function(response) {       //  - .   },   async: false //    - ,      });
      
      





ここではAjaxリクエストの例を示しましたが、どのコードでも非同期モードで実行できます。



たとえば、 setTimeout(callback, milliseconds)



関数を使用してこれを行うことができます。 この関数にアクセスする瞬間より後に発生するイベントの実行をスケジュールすることができます(タイムアウトを設定することにより)。 例を考えてみましょう:



 function first() {   console.log('first'); } function second() {   console.log('second'); } function third() {   console.log('third'); } first(); setTimeout(second, 1000); //   second  1000  third();
      
      





このコードがコンソールに出力する内容は次のとおりです。



 first third second
      
      





イベントループ研究



これは奇妙に思えるかもしれませんが、ES6 JavaScriptより前では、非同期呼び出し(上記のsetTimeout



など)を行うことができましたが、組み込みの非同期プログラミングメカニズムは含まれていませんでした。 JSエンジンは、特定のコードフラグメントを一度に1つずつシングルスレッドで実行するだけでした。



JavaScriptエンジン(特にV8)の仕組みについて詳しくは、 この資料をご覧ください。



それでは、プログラムの一部を実行する必要があることをJSエンジンに誰が伝えるのでしょうか? 実際には、エンジンは単独では動作しません。独自のコードは特定の環境内で実行されます。ほとんどの開発者にとっては、ブラウザーまたはNode.jsです。 実際、今日ではロボットからスマート電球まで、さまざまな種類のデバイス用のJSエンジンがあります。 そのような各デバイスは、JSエンジンの環境の独自のバージョンを表します。



このような環境すべてに共通する特性は、イベントループと呼ばれる組み込みメカニズムです。 プログラムフラグメントの実行をサポートし、このためにJSエンジンを呼び出します。



つまり、エンジンは、オンデマンドで呼び出されるJSコードのランタイムと見なすことができます。 また、イベントプランニング(つまり、JSコード実行セッション)は、エンジン外部の環境メカニズムによって処理されます。



したがって、たとえば、プログラムがAjaxリクエストを実行してサーバーからデータをダウンロードする場合、コールバック内のresponse



変数にこのデータを書き込むコマンドを書くと、JSエンジンは環境に次のように伝えます。このネットワーク要求の実行を完了してデータを受信したら、このコールバックを呼び出してください。



次に、ブラウザーは、ネットワークサービスからの応答を待機しているリスナーを設定し、要求を実行したプログラムに返すことができるものがある場合、コールバックを呼び出してイベントループに追加することを計画します。



次の図をご覧ください。



ヒープ(メモリヒープ)およびコールスタック(コールスタック)の詳細については、 こちらをご覧ください 。 Web APIとは何ですか? 一般に、これらは直接アクセスできないフローであり、呼び出しのみを実行できます。 これらはブラウザに組み込まれ、非同期アクションが実行されます。



Node.js用に開発している場合、C ++を使用して同様のAPIが実装されます。



では、イベントループとは何ですか?









イベントループは、1つの主な問題を解決します。コールスタックとコールバックキューを監視します。 呼び出しスタックが空の場合、ループはキューから最初のイベントを取得してスタックに配置します。これにより、このイベントが実行されます。



この反復は、イベントループのティックと呼ばれます。 各イベントは単なるコールバックです。



次の例を考えてみましょう。



 console.log('Hi'); setTimeout(function cb1() {   console.log('cb1'); }, 5000); console.log('Bye');
      
      





このコードを段階的に「実行」して、システムで何が起こるかを見てみましょう。



1.まだ何も起きていません。 ブラウザコンソールはクリーンで、呼び出しスタックは空です。





2. console.log('Hi')



コマンドが呼び出しスタックに追加されます。





3. console.log('Hi')



コマンドが実行されます。





4. console.log('Hi')



コマンドは呼び出しスタックから削除されます。





5. setTimeout(function cb1() { ... })



コマンドがコールスタックに追加されます。





6. setTimeout(function cb1() { ... })



コマンドが実行されます。 ブラウザは、Web APIの一部であるタイマーを作成します。 彼はカウントダウンを行います。





7. setTimeout(function cb1() { ... })



コマンドが完了し、コールスタックから削除されました。





8. console.log('Bye')



コマンドが呼び出しスタックに追加されます。





9. console.log('Bye')



コマンドが実行されます。





10. console.log('Bye')



コマンドは呼び出しスタックから削除されます。





11.少なくとも5000ミリ秒がcb1



と、タイマーが終了し、cb1 cb1



がコールバックキューに配置されます。





12.イベントループは、 cb1



キューからcb1



関数をcb1



し、コールスタックに配置します。





13. cb1



関数cb1



実行され、 console.log('cb1')



が呼び出しスタックに追加されます。





14. console.log('cb1')



コマンドconsole.log('cb1')



が実行されます。





15. console.log('cb1')



コマンドconsole.log('cb1')



は呼び出しスタックから削除されます。





16. cb1



関数は呼び出しスタックから削除されます。





ここでは、修正のために、アニメーション形式で同じ。





ES6仕様では、イベントループの動作方法が定義されていることに注意してください。つまり、技術的にはJSエコシステムでより重要な役割を果たし始めるJSエンジンの責任内にあることを示しています。 これの主な理由は、約束がES6に登場し、イベントループキューで操作をスケジュールするための信頼できるメカニズムが必要だからです。



setTimeout(...)の仕組み



setTimeout(…)



呼び出しても、コールバックはイベントループキューに自動的に配置されません。 このコマンドはタイマーを開始します。 タイマーがトリガーされると、環境はコールバックをイベントループに入れます。その結果、将来のティックの一部で、このコールバックが機能して実行されます。 このコードスニペットをご覧ください。



 setTimeout(myCallback, 1000);
      
      





このコマンドの実行は、 myCallback



が1000ミリ秒後に実行されることを意味するものではありません。1000ミリ秒後に言った方が正確です。 myCallback



がキューに追加されます。 ただし、キューには他のイベントが以前に追加されている可能性があるため、コールバックは待機する必要があります。



JavaScriptで非同期プログラミングを始めたばかりの人向けの記事がかなりあります。 それらの中でsetTimeout(callback, 0)



コマンドを使用するための推奨事項を見つけることができます。 これで、イベントループの仕組みとsetTimeout



を呼び出したときの動作がわかりました。 これを考えると、2番目の引数0を指定してsetTimeout



を呼び出すと、コールスタックがクリアされるまでコールバック呼び出しが単に延期されることは明らかです。



次の例を見てください。



 console.log('Hi'); setTimeout(function() {   console.log('callback'); }, 0); console.log('Bye');
      
      





タイマーが設定される時間は0ミリ秒ですが、コンソールには次のように表示されます。



 Hi Bye callback
      
      





ES6クエスト



ES6では、ジョブキューと呼ばれる新しいコンセプトが導入されています。 この設計は、イベントループキューの最上位にあるレイヤーと見なすことができます。 promiseの非同期動作の特性に対処する必要があるときに、この問題に遭遇した可能性は十分にあります。



これを一言で説明します。その結果、Promiseを使用した非同期開発について話すとき、非同期アクションがどのように計画および処理されるかを理解できます。



これを想像してください。ジョブキューは、イベントループキューの各ティックの終わりに接続されるキューです。 イベントループのティック中に発生する可能性のある一部の非同期アクションは、新しいイベントをイベントループキューに追加しませんが、代わりに要素(つまり、タスク)が現在のティックのタスクキューの最後に追加されます。 これは、将来実行されるコマンドをキューに追加することにより、実行される順序を確認できることを意味します。



タスクを完了すると、同じキューの最後にタスクを追加できます。 理論的には、「循環」タスク(他のタスクの追加に関与するタスク)が無限に動作し、イベントサイクルの次のティックに移動するために必要なプログラムリソースを使い果たす可能性があります。 概念的には、これはwhile(true)



ような無限ループを作成するようなものです。



タスクは、「hack」s etTimeout(callback, 0)



ようなものですが、後で実行されるができるだけ早く実行される一連の操作を追跡できるように実装されています。



コールバック



既にご存じのとおり、コールバックは、JavaScriptプログラムで非同期アクションを表現および実行する最も一般的な手段です。 さらに、コールバックは最も基本的な非同期言語テンプレートです。 数え切れないほどのJSアプリケーションは、最も独創的で複雑でも、コールバックのみに基づいています。



これはすべて良いことですが、コールバックは完全ではありません。 したがって、多くの開発者は、より成功した非同期開発パターンを見つけようとしています。 しかし、彼らが言うように、内部ですべてがどのように機能するかを理解せずに抽象化を効果的に使用することは不可能です。



以下では、このような抽象概念をいくつか詳しく調べて、さらに詳しく説明するより高度な非同期テンプレートが必要であり、使用が推奨される理由を示します。



ネストされたコールバック



次のコードを見てください。



 listen('click', function (e){   setTimeout(function(){       ajax('https://api.example.com/endpoint', function (text){           if (text == "hello") {       doSomething();   }   else if (text == "world") {       doSomethingElse();           }       });   }, 500); });
      
      





相互に埋め込まれた3つの関数のチェーンがあり、それぞれが非同期に実行される一連のアクションのステップを表します。



このコードはしばしばコールバック地獄と呼ばれます。 しかし、「地獄」は、関数が入れ子になっているという事実や、コードブロックを相互に位置合わせする必要があるという事実にはありません。 これははるかに深い問題です。



このコードを分析しましょう。 まず、 click



イベントを待ち、次にタイマーが起動するのを待ちます。最後に、Ajax応答が到着するのを待ちます。その後、これらすべてが再び発生します。



一見すると、このコードは非同期の性質を連続したステップの形で非常に自然に表現しているように見えるかもしれません。 最初のステップは次のとおりです。



 listen('click', function (e) { // .. });
      
      





次に2つ目を示します。



 setTimeout(function(){   // .. }, 500);
      
      





3つ目は次のとおりです。



 ajax('https://api.example.com/endpoint', function (text){   // .. });
      
      





そして最後に、これが起こることです:



 if (text == "hello") {   doSomething(); } else if (text == "world") {   doSomethingElse(); }
      
      





したがって、非同期コードを記述するための同様のアプローチは、はるかに自然に思えますよね? そのように書く方法がなければなりません。



約束



次のコードスニペットをご覧ください。



 var x = 1; var y = 2; console.log(x + y);
      
      





ここではすべてが非常に簡単です。変数x



およびy



追加され、コンソールに表示されます。 しかし、 x



またはy



値が利用できず、まだ設定されていない場合はどうでしょうか? サーバーからx



y



書き込まれる内容を取得し、このデータを式で使用する必要があるとしましょう。 サーバーからx



y



値をそれぞれダウンロードする関数loadX



loadY



があると想像してください。 次に、 x



y



値がロードされるとすぐにそれらを加算するsum



関数があることを想像してください。



それはすべてこのように見えます(恐ろしいことが起こりましたよね?):



 function sum(getX, getY, callback) {   var x, y;   getX(function(result) {       x = result;       if (y !== undefined) {           callback(x + y);       }   });   getY(function(result) {       y = result;       if (x !== undefined) {           callback(x + y);       }   }); } //    ,   `x` function fetchX() {   // .. } //    ,    `y` function fetchY() {   // .. } sum(fetchX, fetchY, function(result) {   console.log(result); });
      
      





非常に重要なことが1つあります。 つまり、このコードでは、 x



y



を将来受け取る値として扱い、操作のsum(…)



(実装の詳細に入らずに呼び出された場合)を重要ではないように記述します呼び出されたときにx



y



かどうか。



もちろん、ここで紹介する失礼なコールバックアプローチには、多くの要望が残されています。 これは、出現の特定の時間を気にせずに「将来の価値」で動作することを可能にする利点を理解するための最初の小さなステップにすぎません。



約束値



最初に、Promiseを使用してx + y



操作をどのように表現できるかを見てみましょう。



 function sum(xPromise, yPromise) { // `Promise.all([ .. ])`   , //    ,   //   ,     return Promise.all([xPromise, yPromise]) //     ,  //   `X`  `Y`   . .then(function(values){ // `values` -      //   return values[0] + values[1]; } ); } // `fetchX()`  `fetchY()`    //  .     //  **  **. sum(fetchX(), fetchY()) //      //  . //     `then(...)`    //    . .then(function(sum){   console.log(sum); });
      
      





この例では、2つの約束の層があります。



fetchX()



およびfetchY()



呼び出しは直接実行され、それらが返す値(約束!)はsum(...)



渡されsum(...)



。 これらのプロミスが表す値は、現在または後でさらに使用する準備ができている場合がありますが、各プロミスは値の可用性の瞬間がそれ自体では重要ではないように動作します。 その結果、時間を参照せずにx



y



値について説明します。 これらは将来の意味です。



promiseの2番目のレイヤーは、 sum(…)



呼び出しを作成して返す( Promise.all([ ... ])



を使用して) Promise.all([ ... ])



sum(…)



then(…)



呼び出すことにより、このpromiseが返す値を期待します。 sum(…)



操作が完了すると、合計の将来の価値が準備でき、それを表示できます。 sum(…)



内に将来のx



y



値を期待するロジックを隠しsum(…)







sum(…)



内部でPromise.all([ … ])



を呼び出すと、promise( promiseX



およびpromiseY



待っているPromise.all([ … ])



が作成されることに注意してください。 .then(…)



, values[0] + values[1]



( , ). , then(…)



, , , sum(…)



, , , Promise.all([ ... ])



. , then(…)



, , , , . , .then(…)



.



then(…)



, , . — , . — , :



 sum(fetchX(), fetchY()) .then(   //       function(sum) {       console.log( sum );   },   //      function(err) {  console.error( err ); // -    } );
      
      





x



y



- , , , s um(…)



, . , then(…)



, . .



, , — , , , , . , , , .



, , . () , , .



, :



 function delay(time) {   return new Promise(function(resolve, reject){       setTimeout(resolve, time);   }); } delay(1000) .then(function(){   console.log("after 1000ms");   return delay(2000); }) .then(function(){   console.log("after another 2000ms"); }) .then(function(){   console.log("step 4 (next Job)");   return delay(5000); }) // ...
      
      





delay(2000)



, 2000 ., then(…)



, , then(…)



2000 .



, , , , , , . , , . . , , , , , .



?



, , , Promise



. , , , .



, new Promise(…)



, , , p instanceof Promise



. , .



, ( , ), Promise



, , . , , .



, - , , ES6. , , , , .





, TypeError



ReferenceError



, .



例:



 var p = new Promise(function(resolve, reject){   foo.bar(); // `foo`  ,   !   resolve(374);  //      :( }); p.then(   function fulfilled(){       //      :(   },   function rejected(err){       // `err`    `TypeError`       //   `foo.bar()`.   } );
      
      





, , JS- ( , then(…)



)? , , , , :



 var p = new Promise( function(resolve,reject){ resolve(374); }); p.then(function fulfilled(message){   foo.bar();   console.log(message);   //      },   function rejected(err){       //     } );
      
      





, , foo.bar()



«». , . , , . , p.then(…)



, TypeError



.





, .

, , done(…)



, , «». done(…)



, , done(…)



, , , , .



, , , : done(…)



( ):



 var p = Promise.resolve(374); p.then(function fulfilled(msg){   //     ,   //        console.log(msg.toLowerCase()); }) .done(null, function() {   //    ,       });
      
      





ES8: async / await



JavaScript ES8 async / await



, . , async / await



, .



, async



. AsyncFunction . , , .



, Promise



. , Promise



, , . , async



, , .



, async



, await



, , . async- , , , .



Promise JavaScript Future Java C#.



async / await



, .

:



 //  -   JS- function getNumber1() {   return Promise.resolve('374'); } //      ,   getNumber1 async function getNumber2() {   return 374; }
      
      





, , , :



 function f1() {   return Promise.reject('Some error'); } async function f2() {   throw 'Some error'; }
      
      





await



, async



. . async-, then



:



 async function loadData() {   // `rp`-   request-promise.   var promise1 = rp('https://api.example.com/endpoint1');   var promise2 = rp('https://api.example.com/endpoint2');    //         //      .   var response1 = await promise1;   var response2 = await promise2;   return response1 + ' ' + response2; } //       ,     `async` //    `then`    Promise loadData().then(() => console.log('Done'));
      
      





« », function



. , , , , . IIFE (Immediately Invoked Function Expression, ), .



次のようになります。



 var loadData = async function() {   // `rp`-   request-promise.   var promise1 = rp('https://api.example.com/endpoint1');   var promise2 = rp('https://api.example.com/endpoint2');    //         //      .   var response1 = await promise1;   var response2 = await promise2;   return response1 + ' ' + response2; }
      
      





, async / await



.







, , — Babel TypeScript .



async / await



, , .



5 ,





async / await



. , async / await



, . — .then()



, , - , , .



, :



 / `rp`-   request-promise. rp('https://api.example.com/endpoint1').then(function(data) { // … });
      
      





, async / await



:



 // `rp`-   request-promise. var response = await rp('https://api.example.com/endpoint1');
      
      







async / await



. , try / catch



.



, . , .catch()



, try / catch



:



 function loadData() {   try { //   .       getJSON().then(function(response) {           var parsed = JSON.parse(response);           console.log(parsed);       }).catch(function(e) { //              console.log(e);       });   } catch(e) {       console.log(e);   } }
      
      





async / await



:



 async function loadData() {   try {       var data = JSON.parse(await getJSON());       console.log(data);   } catch(e) {       console.log(e);   } }
      
      







async / await



, . — , :



 function loadData() { return getJSON()   .then(function(response) {     if (response.needsAnotherRequest) {       return makeAnotherRequest(response)         .then(function(anotherResponse) {           console.log(anotherResponse)           return anotherResponse         })     } else {       console.log(response)       return response     }   }) }
      
      





async / await



:



 async function loadData() { var response = await getJSON(); if (response.needsAnotherRequest) {   var anotherResponse = await makeAnotherRequest(response);   console.log(anotherResponse)   return anotherResponse } else {   console.log(response);   return response;    } }
      
      







async / await



, , , , . :



 function loadData() { return callAPromise()   .then(callback1)   .then(callback2)   .then(callback3)   .then(() => {     throw new Error("boom");   }) } loadData() .catch(function(e) {   console.log(err); // Error: boom at callAPromise.then.then.then.then (index.js:8:13) });
      
      





— , async / await



:



 async function loadData() { await callAPromise1() await callAPromise2() await callAPromise3() await callAPromise4() await callAPromise5() throw new Error("boom"); } loadData() .catch(function(e) {   console.log(err);   //    // Error: boom at loadData (index.js:7:9) });
      
      







, , — . , .then



«step-over», .then



, «» . async / await



, await



, — .



まとめ



, . , SessionStack , -. , DOM, , JavaScript, , , .



- . , , , , . . , , , .



, . JavaScript, , . , , , .



親愛なる読者! JS-. , - . async / await



. , , , . ( ) a sync / await



.



All Articles