![](https://habrastorage.org/storage2/48c/e72/eae/48ce72eae915dc06bebe4eaccdf4e7cb.gif)
私たちは皆、ajaxとnode.jsについて聞いたことがあります。 彼らは語彙だけでなく、ウェブ開発者ツールキットでもしっかりと定着しています。 Ajax-サーバーからページへのデータの非同期プル、ノード-非同期IOを備えたフレームワーク。 しかし、Javascriptのようなシングルスレッド言語は、どのように同じ非同期性を実装するのでしょうか?
おそらくタイトルからすでに推測されているように、メインループ(「メインループ」)について説明します。
仕様通り
遠くから始めましょう。 「Javascript」、「ECMAScript」、「ES」、「JS」を意味します(もちろん、あなたがMozillaで働いていて、Brendan Aichと呼ばれていない場合を除きます)。 ECMAScript仕様から始めることは論理的です。
ECMA-262を開くと、スクリプトが機能した後に何をすべきか、スクリプトを一時停止または停止する方法について何も言われていないことがわかります。 これは、これが実行される環境に残されることを意味します。
WSHで行われる方法
おそらく最も簡単な環境は、Windows Script Host、別名WSHです。 私はプロセッサを100%ロードし、最後まで働き、死にました-これはとても単純なライフサイクルです。 実行制御機能のうち、古くからある
sleep()
のみがインタープリターをnミリ秒間停止します。
WScript.echo(new Date() + ': Hello world!'); WScript.sleep(1000); WScript.echo(new Date() + ': Goodbye, cruel world!');
単純なタスクの場合はこれで十分ですが、より困難なタスクは障害物コースになる可能性があります。
天井からの例:ライブラリには、キーボードのないWindows 98上に端末がありますが、Internet Explorer 6には、書籍のカタログが全画面で表示されます。 若いフーリガンはIE6をシャットダウンしたがり、図書館員がIE6を立ち上げるのは容易ではありませんでした。 になる方法
IEを起動し、トリッキーな名前でグローバルオスプレイにコールバックを作成して実行する関数を作成します。
function startIE () { var ie = WScript.CreateObject("InternetExplorer.Application", "ieEvent_"); ie.Navigate("http://127.0.0.1/"); ie.Visible = true; ie.FullScreen = true; } var ieEvent_OnQuit = startIE; startIE();
動作しません。 より正確には、IEは起動しますが、インタプリタは最後まで完了し、静かに死にます。 彼を永遠の眠りにつく必要があります。 おでこで、
sleep(Infinity)
WSHはどのように知らないので、60秒間スリップする無限ループを行います。 どういうわけか次のようになります。
function startIE () { var ie = WScript.CreateObject("InternetExplorer.Application", "ieEvent_"); ie.Navigate("http://127.0.0.1/"); ie.Visible = true; ie.FullScreen = true; } var ieEvent_OnQuit = startIE; startIE(); while (true){ WScript.sleep(60000); }
図書館員は幸せになりました。 予想よりもさらに優れています-IEは0〜59秒の任意の時間後にではなく、すぐに再起動します。 「イベント」OnQuitが通訳者の睡眠を中断することがわかりました。 同時に、
sleep
切断され、ビジーループのみが残る場合、エクスプローラーはOnQuitを起動することさえできません。
さて、原始的なメインループがあります。 簡単に説明すると、次のように説明できます。
- 通訳者の仕事は、プログラムの完了で終わりません。
- コールバックコードは、インタープリターが何もしない場合にのみ機能します。 2つのコードを同時に機能させることはできません。
これはブラウザでどのように行われますか
「実現して死んだ」アプローチは、ブラウザーに適合しません。 それ以外の場合、JSを主な目的に使用する方法-どこかをクリックすると迷惑なポップアップを開くには? ブラウザ(およびnode.js)では、メインループが配線されています。 そして非常に深く、ループスクリプトでは、インターフェースを使用することさえ不可能な場合があります。
原則は単純です。キューがあり、実行したいコードはすべてキューに入ります。 キューが空でない場合、インタープリターはキューの最初の要素を噛んで実行します。 キューが空の場合、何かが入るまで待機します。
このようなキューの「コード」は、ページ上の埋め込みおよびリンクされたスクリプト、インターフェースイベント(
onclick
、
onmouseover
、...)、タイマーコールバック(
setTimeout
、
setInterval
)、またはブラウザーオブジェクト(
xhr.onreadystatechange
)の
xhr.onreadystatechange
ん。 ある意味では、ブラウザベースのJSのタイマーとイベントに違いはありません。
さて、順番に。
警戒
3つの機能:
alert
、
prompt
、confirm-ブラウザーベースのjavascript全体で独立しています。 おそらく、私のように、あなたのJSとの知り合いはそのうちの一人から始まったのでしょう。 いずれにしても、それぞれがモーダルウィンドウを作成し、インタープリターは閉じられるまでスリープ状態になります。 これは、ブラウザーのメインループを一時停止する唯一の方法です(デバッガーを使用せずに)。
以前は、ブラウザでは、一部のバックグラウンドタブの
alert
がインターフェース全体をブロックすることがありましたが、Google Chromeは依然としてこれに悩まされています。 ただし、現代のインターネットでは、これらの機能の使用を満たすことができない場合があります。 したがって、それらも使用しません。
setTimeout、setInterval
ブラウザーベースのJSには
sleep
機能はありません-インタープリターは停止しません(
alert
などを除く)。 ただし、
setTimeout
関数を使用して、関数の実行を「完全に」遅らせることができます。
構文はシンプルで簡潔です:
setTimeout(fn, timeout)
。
fn
関数は
timeout
ミリ秒の
timeout
後より早く起動されます。 なぜ直前だけでなく、 正確に通していないのですか? ボンネットの下を見てください。
setTimeout
呼び出しは、新しいタイマーを登録します(ところで、その識別子とこの関数は呼び出されると戻ります)。 時間が経過し、インタープリターがコードの実行に忙しくないことが判明すると、関数
fn
すぐに呼び出されます。これは些細なケースです。
運が悪く、その時点でもJSエンジンがキューの一部を噛んでいる場合は、キューが空になるまで最初に待機する必要があります。 「今すぐ」コールバックを起動したいという希望はすべて機能しないため、Javascriptはシングルスレッドです。 次に、キューが空になると、インタープリターはすべてのタイマーを調べ、どのタイマーが期限切れになったかを確認します。 すべての経過タイマーのうち、短いタイムアウトで設定されたものが選択され、複数ある場合は、すべての前にインストールされたものが選択されます。 現在、そのような「最も期限切れの」タイマーは、実行キューの新しい要素を生成します。そして、-インタプリタは再び何かする必要があります-空でないキューを分解します。
setTimeout
タイマー
setTimeout
切れると、削除されます。 つまり、
setTimeout
は2回
setTimeout
しません。
さて、いくつかの例示的なコード:
console.log('script started'); setTimeout(function(){ console.log('timed out function'); }, 5); var endDate = +new Date() + 10; while (+new Date() < endDate){ // busy loop for 10 ms } console.log('script finished');
コンソールには以下が表示されます。
スクリプト開始 スクリプト終了 タイムアウト機能
その順序で。 タイマーが5ミリ秒後に期限切れになったという事実にもかかわらず、当時のエンジンは、日付と基準を常に比較するという非常に重要なタスクを処理していました。 したがって、遅延関数は、終了するまでさらに5ミリ秒待機する必要がありました。 こっち これがおそらく最も重要です。
clearTimeout(timeoutId)
関数を使用すると、いつでもタイマーをキャンセルできます。 タイマーがすでに発砲している場合、それをキャンセルすることは、一般に、すでに無意味ですが、これはエラーとは見なされません。
タイマーをキャンセルする時間はありますか?
var timeoutId; setTimeout(function(){ console.log('timed out function'); clearTimeout(timeoutId); }, 5); timeoutId = setTimeout(function(){ console.log('timed out function 2'); }, 5); var endDate = +new Date() + 10; while (+new Date() < endDate){ // busy loop for 10 ms }
この例のようなケースは実際にはめったに発生しませんが、それでも発生します。 両方のタイマーは5ミリ秒に設定されており、無意味で容赦のないアクティビティが終了すると、両方ともすでに期限が切れています。 しかし、両方のタイマーの遅延のミリ秒数は同じでしたが、最初に設定されたため、最初のタイマーが最初に「撮影」されます。 また、すでに期限切れになっている2番目のタイマーが正常に削除され、撮影ができなくなります。
setInterval
は、タイマーが実行キューの要素を生成した後に削除されないという点で
setTimeout
と異なります。 代わりに、その値は元の値にリセットされます。 これにより、
setTimeout
内で
setTimeout
を呼び出すことなく、定期的に関数を呼び出すことができます。
setInterval
タイマーカウンターは、タイマーがトリガーされてもリセットされず、メインループキューが空の場合にのみリセットされることに注意してください。 このため、時間とともに「スリップ」する可能性があります。
証明
setInterval(function(){ console.log(+new Date()); }, 1000); setTimeout(function(){ var endDate = +new Date() + 2000; while (+new Date() < endDate){ // busy loop for 2000 ms } }, 1500);
残りは同じです。 間隔のみが、別の関数
clearInterval
によってオーバーライドされます。
最後に、
setTimeout
または
setInterval
に4ミリ秒未満のタイムアウト値が渡された場合、代わりに正確に4ミリ秒が使用されます。 それは私にとって不愉快な驚きでした。 しかし、明らかに、それは良いことです-ランダムに設定された0ミリ秒の間隔は、すべてのブラウザのメインループを迅速かつ効率的に消し去ります。 本当にゼロのタイムアウトで関数を実行するには、
setImmediate
使用し
setImmediate
。 この機能は、「すぐに使える」ものとしてはまだあまり広く利用できませんが、ポリフィルがあります。
<スクリプト>
すべてのスクリプトはメインループキューに分類されます。 一方では、これは
async
と
defer
使用を許可します。 一方、これらの属性がないと、予期しない結果になる可能性があります。
2つのスクリプトがページに埋め込まれ、最初のスクリプトが4ミリ秒のタイムアウトを設定し、その後10ミリ秒遅くなった場合はどうなりますか。 タイムアウトコールバックはいつ機能しますか-2番目のスクリプトの前、または後?
コード
<!DOCTYPE html> <script> console.log('script 1'); setTimeout(function(){ console.log('setTimeout from script 1'); }, 5); var endDate = +new Date() + 10; while (+new Date() < endDate){ // busy loop for 10 ms } console.log('script 1 finished'); </script> <script> console.log('script 2'); </script>
Firefox、Chrome、およびIE10は、2番目のスクリプトの後にタイムアウトします。 Opera-2番目のスクリプトの前。
そして、2番目のスクリプトが埋め込まれていない場合、インラインではなく、外部ですか?
コード
<!DOCTYPE html> <script> console.log('script 1'); setTimeout(function(){ console.log('setTimeout from script 1'); }, 5); var endDate = +new Date() + 10; while (+new Date() < endDate){ // busy loop for 10 ms } console.log('script 1 finished'); </script> <script src="http://127.0.0.1/script.js"></script>
そして、ここではすべてのブラウザーが 2番目のスクリプトの前にタイムアウトする可能性があります。 この知識から、学問的意義を除いて、あなたは何がいいのでしょうか? 要求の数を減らすために、多くの場合、複数のスクリプトが1つに接着されます。 また、結果のスクリプトは元のスクリプトとは異なる動作をする場合があります。
イベント
ユーザーインターフェイスイベントとDOM変更イベント(
onDOMNodeInsertedIntoDocument
など)を個別に検討する価値があります。
DOMツリーが変更されたときに発生するイベントは、メインループに該当しません。 代わりに、ツリーを変更した直後に解決します。
var i = 0; document.body.addEventListener('DOMSubtreeModified', function(e){ i = 42; }, false); console.log('i = ' + i); // i = 0; document.body.appendChild(document.createElement('div')) console.log('i = ' + i); // i = 42;
実生活で変数の値がこのように自発的に変化するのは難しいようです。 しかし、インターネット上のどこかで、特にトリッキーなjQueryプラグインが翼で待っており、MutationEventを積極的に使用し、グローバルオスプレイに流れ込んでいると思います。 さらに、DOM変更イベントは推奨されておらず(非推奨)、ツリーでの作業を大幅に禁止しているため、使用しないでください。
mouse-keyboard-tachevymのイベントに戻りましょう。 そのような各イベントにはデフォルトのアクションがあります。 たとえば、
mousedown
イベントのボタンのデフォルトアクションは、押し下げられたビューを取得することです。 そして、クリックによるリンクのために-アドレスに行きます。 処理関数の最初の引数でpreventDefaultメソッド
preventDefault
呼び出すことにより、一部のデフォルトアクションを元に戻すことができます。
そして、それは2つのことを意味します。 まず、最初にハンドラー、次にデフォルトのアクション、そしてボタンはJavascriptハンドラーがパスするまで「クリック」しません。 次に、メインループの順番が近づくまで、ハンドラーは開始しません。 したがって、jsエンジンの大規模な計算は、応答性と呼ばれるものに非常に悪影響を及ぼします。 言い換えれば、すべてが大幅に遅くなり、ユーザーを悩ますようになります。 ハンドラーがハングしていないイベントを待つことは論理的に思えますが、実際、すべてのブラウザーで
onclick
あるボタンは、それがない場合と同じように遅くなります。
もちろん、ブラウザ開発者はそれについて何かをしようとしています。 たとえば、Operaはハンドラーを待たずに視覚的な表現を変更します。これにより、ブレーキをわずかに滑らかにすることができます。 また、ページは隣接するタブでの難しいプロセスの影響を受ける可能性がありますが、これはChromeには適用されません。
keypress
イベント
keypress
入力フィールド内のテキストの長さを決定する
keypress
場合、シーケンス「最初のハンドラー、次にデフォルトアクション」は依然として標準的なケースに従いますが、実際のテキストと一致しないことがわかります。 この問題の解決策は読者にお任せします。
ウェブワーカー
WebWorkersは、最初にメインループをオフロードして、ユーザーインターフェイスの応答性の「沈下」を取り除く方法です。 実際、これは独自のメインループを備えた別個のプロセスであり、インターフェイスとDOMに結び付けられていません(そしてそれらに直接アクセスすることもできません)。
WebWorkerは、メッセージを介して「コア」プロセスと排他的に通信します。 さらに、歩行者からのメッセージがメインプロセスに到達すると、それは-はい、はい-他のすべてと同じキューになります。
一般に、クライアントでxmからwavを生成するか、マルチメガバイトのbzipを解凍する必要があり、ブラウザーがWebWorkersをサポートしている場合は、それらを使用します。
node.jsでこれを行う方法
ノードは非常に類似したブラウザモデルを使用するため、このセクションは非常に小さくなります。 同じ
setTimeout
と
setInterval
、およびファイルシステムと他のIOのイベントは、引数が異なることを除いて、ブラウザイベントに似ています。
機能のうち、関数
process.nextTick
に注目できます。これは
setTimeout(…, 0)
ように機能し
process.nextTick
が、不要なタイマーは作成しません。 node.jsタイマーの識別子は、ブラウザのように整数ではなく、オブジェクトです。 さて、ノードは4ミリ秒の制限を完全に無視します。
結論の代わりに
上記のすべてを要約します。メインサイクルがどのように機能するかを理解し、それを長時間遅らせないようにします。
さらに読む/表示するには: