JavaScriptタイマー:知っておくべきこと

こんにちは同僚。 むかしむかし、このトピックに関するHabreの記事がJohn Rezigによって執筆されました。 10年が経過しましたが、このトピックにはまだ説明が必要です。 したがって、Samer Bunaの記事を読むことに興味がある人には、JavaScriptのタイマー(Node.jsのコンテキスト内)の理論的な概要だけでなく、それらに関するタスクも提供します。









数週間前、私は単一のインタビューから次の質問をツイートしました。



「setTimeoutおよびsetInterval関数のソースコードはどこにありますか? 彼をどこで探しますか? Googleで検索することはできません:) "



***自分で答えてから読んでください***






このツイートへの回答の約半分は間違っていました。 いいえ、ケースはV8(または他のVM)とは関係ありません!!! JavaScript JavaScriptタイマーと呼ばれるsetTimeout



setInterval



などのsetTimeout



は、ECMAScript仕様またはJavaScriptエンジン実装の一部ではありません。 タイマー関数はブラウザーレベルで実装されるため、ブラウザーによって実装が異なります。 タイマーもNode.jsランタイム自体にネイティブに実装されます。



ブラウザでは、メインタイマー関数はWindow



インターフェイスに関連しています。これは、他のいくつかの関数やオブジェクトにも関連付けられています。 このインターフェイスは、JavaScriptのメインスコープ内のすべての要素へのグローバルアクセスを提供します。 これが、ブラウザコンソールでsetTimeout



関数を直接実行できる理由です。



Nodeでは、タイマーはglobal



オブジェクトの一部であり、 Window



ブラウザインターフェースのように構成されています。 Nodeのタイマーのソースコードを次に示します



これはインタビューからの単なる悪い質問であると誰かに思われるかもしれません-そのようなことを知ることはどのような用途ですか?! JavaScript開発者として、私はこのように考えます。V8(および他の仮想マシン)がブラウザーやノードと対話する方法を十分に理解していないことを反対が示す可能性があるため、これを知っている必要があると想定されています。



いくつかの例を見て、いくつかのタイマータスクを解決しましょう。



nodeコマンドを使用して、この記事の例を実行できます。 ここで説明する例のほとんどは、PluralsightのNode.js入門コースで取り上げられました。



遅延関数実行



タイマーは、他の関数の実行を遅延または繰り返すことができる高次関数です(タイマーはそのような関数を最初の引数として受け取ります)。



遅延実行の例を次に示します。



 // example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 );
      
      





この例では、 setTimeout



を使用して、グリーティングメッセージが4秒間遅延します。 setTimeout



の2番目の引数は遅延(ミリ秒単位)です。 4を得るために4を1000倍します。



setTimeout



の最初の引数は、実行が遅延する関数です。

nodeコマンドでexample1.js



ファイルを実行すると、Nodeは4秒間一時停止し、ウェルカムメッセージを表示します(その後に終了が続きます)。



setTimeout



の最初の引数は関数への単なる参照であることに注意してください。 example1.js



などの組み込み関数であってはなりません。 以下は、組み込み関数を使用しない同じ例です。



 const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000);
      
      





引数を渡す



遅延にsetTimeout



を使用するsetTimeout



が引数を取る場合、 setTimeout



関数自体の残りの引数(既に学習した2つ後)を使用して、引数の値を遅延関数に転送できます。



 // : func(arg1, arg2, arg3, ...) //  : setTimeout(func, delay, arg1, arg2, arg3, ...)
      
      





以下に例を示します。



 // example2.js const rocks = who => { console.log(who + ' rocks'); }; setTimeout(rocks, 2 * 1000, 'Node.js');
      
      





上記のrocks



関数は、2秒遅延し、 who



引数を取り、 setTimeout



を呼び出すと、そのようなwho



引数として値「Node.js」を渡します。



node



コマンドでexample2.js



を実行すると、「Node.js rocks」というフレーズが2秒後に表示されます。



タイマータスク#1



したがって、 setTimeout



について既に検討した資料に基づいて、対応する遅延の後に次の2つのメッセージを表示します。





制限



ソリューションでは、組み込み関数を含む関数を1つだけ定義できます。 これは、多くのsetTimeout



呼び出しが同じ関数を使用する必要があることを意味します。



解決策



この問題を解決する方法は次のとおりです。



 // solution1.js const theOneFunc = delay => { console.log('Hello after ' + delay + ' seconds'); }; setTimeout(theOneFunc, 4 * 1000, 4); setTimeout(theOneFunc, 8 * 1000, 8);
      
      





私にとって、 theOneFunc



delay



引数を受け取り、画面に表示されるメッセージでこのdelay



引数の値を使用します。 したがって、関数は、遅延の値に応じて異なるメッセージを表示できます。



次に、2つのsetTimeout



呼び出しでtheOneFunc



を使用しました。最初の呼び出しは4秒後に起動され、2番目の呼び出しは8秒後に起動されました。 これらのsetTimeout



呼び出しは両方とも、 theOneFunc



delay



引数を表す3番目の引数も受け取ります。



nodeコマンドでsolution1.js



ファイルを実行すると、タスクの要件が表示されます。さらに、最初のメッセージは4秒後に表示され、2番目は8秒後に表示されます。



機能を繰り返します



しかし、無制限の時間、4秒ごとにメッセージを表示するように要求した場合はどうなりますか?

もちろん、 setTimeout



をループで囲むこともできますが、タイマー関数APIにはsetInterval



関数も用意されており、これを使用して任意の操作の「永遠の」実行をプログラムできます。



setInterval



例を次に示しsetInterval







 // example3.js setInterval( () => console.log('Hello every 3 seconds'), 3000 );
      
      





このコードは3秒ごとにメッセージを表示します。 node



コマンドを使用してexample3.js



を実行すると、プロセスを強制終了するまで(CTRL + C)、Nodeはこのコマンドを出力します。



タイマーをキャンセルする



タイマー関数が呼び出されるとアクションが割り当てられるため、このアクションは実行前に元に戻すこともできます。



setTimeout



呼び出しはタイマーIDを返し、 clearTimeout



を呼び出してタイマーをキャンセルするときにこのタイマーIDを使用できます。 以下に例を示します。



 // example4.js const timerId = setTimeout( () => console.log('You will not see this one!'), 0 ); clearTimeout(timerId);
      
      





この単純なタイマーは0ミリ秒後に(つまり、すぐに) timerId



しますが、 timerId



の値をキャプチャし、 clearTimeout



を呼び出してこのタイマーを直ちにキャンセルするため、これは発生しません。



node



コマンドでexample4.js



を実行すると、Nodeは何も出力しません-プロセスはただちに終了します。



ちなみに、Node.jsには、 setTimeout



の値を0ミリ秒に設定する別の方法も用意されています。 Node.jsタイマーAPIにはsetImmediate



と呼ばれる別の関数があり、基本的に0ミリ秒の値でsetTimeout



と同じことを行いますが、この場合は遅延を省略できます。



 setImmediate( () => console.log('I am equivalent to setTimeout with 0 ms'), );
      
      





setImmediate



関数は、すべてのブラウザーでサポートされているsetImmediate



はありsetImmediate



。 クライアントコードでは使用しないでください。



clearTimeout



とともに、同じことを行うclearInterval



関数がありますが、 setInerval



呼び出しがあり、 clearImmediate



呼び出しもあります。



タイマー遅延-保証されていないこと



前の例で、0 ms後にsetTimeout



操作を実行すると、この操作はすぐに( setTimeout



後)発生せず、すべてのスクリプトコードが完全に実行された後( clearTimeout



呼び出しを含む)にのみ発生します。



例でこの点を明確にしましょう。 0.5秒で動作するはずの単純なsetTimeout



呼び出しを次に示しますが、これは起こりません。



 // example5.js setTimeout( () => console.log('Hello after 0.5 seconds. MAYBE!'), 500, ); for (let i = 0; i < 1e10; i++) { //    }
      
      





この例でタイマーを定義した直後に、大きなfor



ループでランタイム環境を同期的にブロックします。 1e10



の値は1であり、10個のゼロがあるため、サイクルは100億プロセッササイクル続きます(原則として、これは過負荷のプロセッサをシミュレートします)。 このループが完了するまで、ノードは何もできません。



もちろん、実際にはこれは非常に悪いですが、この例はsetTimeout



遅延が保証されているのではなく、 最小値であることを理解するのに役立ちます 。 500 msの値は、遅延が少なくとも500 ms続くことを意味します。 実際、スクリプトは画面にウェルカムラインを表示するのにかなり時間がかかります。 まず、ブロッキングサイクルが完了するまで待つ必要があります。



タイマーの問題#2



「Hello World」メッセージを1秒に1回表示するが、5回だけ表示するスクリプトを作成します。 5回の反復後、スクリプトは「完了」メッセージを表示し、その後ノードプロセスが完了します。



制限 :この問題を解決するとき、 setTimeout



呼び出すことはできません。



ヒント :カウンターが必要です。



解決策



この問題を解決する方法は次のとおりです。



 let counter = 0; const intervalId = setInterval(() => { console.log('Hello World'); counter += 1; if (counter === 5) { console.log('Done'); clearInterval(intervalId); } }, 1000);
      
      





counter



の初期値として0を設定してから、idを取得counter



setInterval



ました。



遅延関数はメッセージを表示し、そのたびにカウンターを1つ増やします。 遅延関数の内部には、ifステートメントがあり、5回の反復が既に通過したかどうかを確認します。 5回の反復後、プログラムは「完了」を表示し、キャプチャされたintervalId



定数を使用して間隔値をクリアします。 間隔遅延は1000ミリ秒です。



遅延関数を正確に呼び出すのは誰ですか?



JavaScriptを通常の関数内でthis



を使用する場合、たとえば次のようになります。



 function whoCalledMe() { console.log('Caller is', this); }
      
      





this



値は、 呼び出し元と一致します。 Node REPL内で上記の関数を定義すると、 global



オブジェクトがそれを呼び出します。 ブラウザコンソールで関数を定義すると、 window



オブジェクトがその関数を呼び出します。



関数をオブジェクトのプロパティとして定義して、少しわかりやすくしましょう。



 const obj = { id: '42', whoCalledMe() { console.log('Caller is', this); } }; //     : obj.whoCallMe
      
      





これで、 obj.whoCallMe



関数を使用してリンクを直接使用すると、 obj



オブジェクト( id



識別される)が呼び出し元として機能します。







ここでの質問は、 obj.whoCallMe



へのリンクをobj.whoCallMe



に渡すと、誰が発信者になるのかということです。



 //       ?? setTimeout(obj.whoCalledMe, 0);
      
      





この場合の発信者は誰ですか?



答えは、タイマー機能が実行される場所によって異なります。 この場合、呼び出し元が誰であるかへの依存は、単に受け入れられません。 この場合、関数を呼び出すタイマーの実装に依存するため、呼び出し元の制御が失われます。 Node REPLでこのコードをテストすると、 Timeout



オブジェクトが呼び出し元になります。







注:これは、JavaScript this



通常の関数内this



使用される場合にのみ重要です。 矢印関数を使用する場合、呼び出し元はまったく気にしません。



タイマータスク#3



さまざまな遅延で「Hello World」メッセージを継続的に出力するスクリプトを作成します。 1秒の遅延で開始し、各反復で1秒ずつ増やします。 2回目の反復では、遅延は2秒になります。 3番目-3など。



表示されるメッセージに遅延を含めます。 次のようなものが得られるはずです。



Hello World. 1

Hello World. 2

Hello World. 3

...








制限 :変数はconstを使用してのみ定義できます。 letまたはvarを使用することはできません。



解決策



このタスクの遅延時間は変数であるため、ここではsetInterval



使用できませんが、再帰呼び出し内でsetTimeout



を使用して間隔の実行を手動で構成できます。 setTimeout



最初に実行されるsetTimeout



は、次のタイマーを作成します。



さらに、 let



/ var



使用できないため、再帰呼び出しごとに遅延をインクリメントするカウンターを使用できません。 代わりに、再帰関数の引数を使用して、再帰呼び出し中にインクリメントできます。



この問題を解決する方法は次のとおりです。



 const greeting = delay => setTimeout(() => { console.log('Hello World. ' + delay); greeting(delay + 1); }, delay * 1000); greeting(1);
      
      





タイマータスク#4



タスク#3と同じ遅延構造を持つ「Hello World」メッセージを表示するスクリプトを作成しますが、今回は5つのメッセージのグループであり、グループにはメインの遅延間隔があります。 5つのメッセージの最初のグループでは、最初の遅延を100ミリ秒、次の場合は200ミリ秒、3番目の場合は300ミリ秒などを選択します。



このスクリプトの仕組みは次のとおりです。





この原則によれば、プログラムは無期限に動作するはずです。



表示されるメッセージに遅延を含めます。 次のようなものが得られるはずです(コメントなし):



Hello World. 100 // 100

Hello World. 100 // 200

Hello World. 100 // 300

Hello World. 100 // 400

Hello World. 100 // 500

Hello World. 200 // 700

Hello World. 200 // 900

Hello World. 200 // 1100

...








制限事項setInterval



呼び出し( setTimeout



ではなく)とif



のみを使用できsetInterval







解決策



setInterval



呼び出しでしか処理できないため、ここでは再帰を使用し、次のsetInterval



呼び出しの遅延を増やす必要があります。 さらに、この再帰関数を5回呼び出した後にのみこれを実行するif



が必要です。



考えられる解決策は次のとおりです。



 let lastIntervalId, counter = 5; const greeting = delay => { if (counter === 5) { clearInterval(lastIntervalId); lastIntervalId = setInterval(() => { console.log('Hello World. ', delay); greeting(delay + 100); }, delay); counter = 0; } counter += 1; }; greeting(100);
      
      





それを読んだすべての人に感謝します。



All Articles