Web ブラウザーのスクリプト言語である JavaScript の人気により、そのイベント駆動型の対話モデルと、Ruby、Python、Java の一般的なリクエスト/レスポンス モデルとの違いについての基本を理解することが有益です。の。この記事では、既に使用しているものの完全には理解していない可能性がある言語についての理解を深められるように、イベント ループやメッセージ キューなど、JavaScript の同時実行モデルの中心的な概念のいくつかを説明します。
この記事は誰に向けて書かれていますか?
この記事は、クライアント側またはサーバー側で JavaScript を使用している、または使用する予定の Web 開発者を対象としています。すでにイベント ループに精通している場合は、この記事のほとんどをよく理解しているはずです。まだ十分に熟練していない人のために、日常的にコードを読んだり書いたりするのに役立つ基本的な理解を提供したいと考えています。
ノンブロッキング I/O
JavaScript では、ほぼすべての I/O がノンブロッキングです。これには、HTTP リクエスト、データベース操作、ディスクの読み取りと書き込みが含まれます。シングルスレッドの実行では、実行時に操作が実行されるときにコールバック関数が提供され、その後他の処理を継続する必要があります。操作が完了すると、メッセージは提供されたコールバック関数とともにキューに挿入されます。将来のある時点で、メッセージはキューから削除され、コールバック関数が起動されます。
このインタラクション モデルは、「マウスダウン」や「クリック」イベントなど、ある時点でトリガーされるユーザー インターフェイスの操作にすでに慣れている開発者には馴染みのあるものかもしれません。これは、サーバー側アプリケーションで通常実行される同期要求/応答モデルとは異なります。
www.google.com に HTTP リクエストを送信し、その応答をコンソールに出力する 2 つの小さなコードを比較してみましょう。まず、Ruby を見て、それを Faraday (Ruby HTTP クライアント開発ライブラリ) で使用してみましょう:
response = Faraday.get 'http://www.google.com' puts response puts 'Done!'
実行パスは簡単に追跡できます:
1. get メソッドを実行し、実行スレッドは応答を受信するまで待機します。
2. 応答は Google から受信され、呼び出し元に返され、変数
に保存されます。
3. 変数の値 (この場合は応答) がコンソールに出力されます
4. 値「Done!」がコンソールに出力されます
Node.js と Request ライブラリを使用して、JavaScript で同じことを実行してみましょう:
request('http://www.google.com', function(error, response, body) { console.log(body); }); console.log('Done!');
表面的には少し異なって見えますが、実際の動作は完全に異なります:
1. リクエスト関数を実行し、匿名関数をコールバックとして渡し、将来応答が得られたときにコールバックを実行します。
2. 「完了!」がすぐにコンソールに出力されます
3. 将来のある時点で、応答が返され、コールバックが実行されたときに、その内容をコンソールに出力します
イベントループ
呼び出し側と応答を分離すると、JavaScript は非同期操作が完了し、実行時にコールバックが起動するのを待機している間に他の処理を行うことができます。しかし、これらのコールバックはメモリ内でどのように編成され、どのような順序で実行されるのでしょうか?何が彼らをそう呼ぶのでしょうか?
JavaScript ランタイムにはメッセージ キューが含まれており、処理する必要があるメッセージと関連するコールバック関数のリストが保存されます。これらのメッセージは、コールバック関数に関係する外部イベント (マウスのクリックや HTTP リクエストへの応答など) に応答してキューに入れられます。たとえば、ユーザーがボタンをクリックし、コールバック関数が提供されていない場合、メッセージはキューに入れられません。
ループでは、キューが次のメッセージをフェッチし (各フェッチは「ティック」と呼ばれます)、イベントが発生すると、メッセージのコールバックが実行されます。
コールバック関数の呼び出しは、コール スタック内の初期化フレーム (フラグメント) として機能します。JavaScript はシングルスレッドであるため、スタック内のすべての呼び出しが返されるまで待機している間、以降のメッセージの抽出と処理は停止されます。後続の (同期) 関数呼び出しでは、新しい呼び出しフレームがスタックに追加されます (たとえば、関数 init は関数 changeColor を呼び出します)。
function init() { var link = document.getElementById("foo"); link.addEventListener("click", function changeColor() { this.style.color = "burlywood"; }); } init();
この例では、ユーザーが「foo」要素をクリックすると、メッセージ (およびそのコールバック関数changeColor) がキューに挿入され、「onclick」イベントがトリガーされます。メッセージがキューから出ると、そのコールバック関数changeColorが呼び出されます。 ChangeColor が返される (またはエラーがスローされる) と、イベント ループが継続します。関数changeColorが存在し、「foo」要素のonclickメソッドへのコールバックとして指定されている限り、その要素をクリックするとさらに多くのメッセージ(および関連するコールバックchangeColor)がキューに挿入されます。
追加メッセージをキューに追加します
コード内で関数が非同期的に呼び出された場合 (setTimeout など)、提供されたコールバックは最終的に別のメッセージ キューの一部として実行され、イベント ループ内の将来のアクションで発生します。例:
function f() { console.log("foo"); setTimeout(g, 0); console.log("baz"); h(); } function g() { console.log("bar"); } function h() { console.log("blix"); } f();
由于setTimeout的非阻塞特性,它的回调将在至少0毫秒后触发,而不是作为消息的一部分被处理。在这个示例中,setTimeout被调用, 传入了一个回调函数g且延时0毫秒后执行。当我们指定时间到达(当前情况是,几乎立即执行),一个单独的消息将被加入队列(g作为回调函数)。控制台打印的结果会是像这样:“foo”,“baz”,“blix”,然后是事件循环的下一个动作:“bar”。如果在同一个调用片段中,两个调用都设置为setTimeout -传递给第二个参数的值也相同-则它们的回调将按照调用顺序插入队列。
Web Workers
使用Web Workers允许您能够将一项费时的操作在一个单独的线程中执行,从而可以释放主线程去做别的事情。worker(工作线程)包括一个独立的消息队列,事件循 环,内存空间独立于实例化它的原始线程。worker和主线程之间的通信通过消息传递,看起来很像我们往常常见的传统事件代码示例。
首先,我们的worker:
// our worker, which does some CPU-intensive operation var reportResult = function(e) { pi = SomeLib.computePiToSpecifiedDecimals(e.data); postMessage(pi); }; onmessage = reportResult;
然后,主要的代码块在我们的HTML中以script-标签存在:
// our main code, in a <script>-tag in our HTML page var piWorker = new Worker("pi_calculator.js"); var logResult = function(e) { console.log("PI: " + e.data); }; piWorker.addEventListener("message", logResult, false); piWorker.postMessage(100000);
在这个例子中,主线程创建一个worker,同时注册logResult回调函数到其“消息”事件。在worker里,reportResult函数注册到自己的“消息”事件中。当worker线程接收到主线程的消息,worker入队一条消息同时带上reportResult回调函数。消息出队时,一条新消息发送回主线程,新消息入队主线程队列(带上logResult回调函数)。这样,开发人员可以将cpu密集型操作委托给一个单独的线程,使主线程解放出来继续处理消息和事件。
关于闭包的
JavaScript对闭包的支持,允许你这样注册回调函数,当回调函数执行时,保持了对他们被创建的环境的访问(即使回调的执行时创建了一个全新的调用栈)。理解我们的回调作为一个不同的消息的一部分被执行,而不是创建它的那个会很有意思。看看下面的例子:
function changeHeaderDeferred() { var header = document.getElementById("header"); setTimeout(function changeHeader() { header.style.color = "red"; return false; }, 100); return false; } changeHeaderDeferred();
在这个例子中,changeHeaderDeferred函数被执行时包含了变量header。函数 setTimeout被调用,导致消息(带上changeHeader回调)被添加到消息队列,在大约100毫秒后执行。然后 changeHeaderDeferred函数返回false,结束第一个消息的处理,但header变量仍然可以通过闭包被引用,而不是被垃圾回收。当 第二个消息被处理(changeHeader函数),它保持了对在外部函数作用域中声明的header变量的访问。一旦第二个消息 (changeHeader函数)执行结束,header变量可以被垃圾回收。
提醒
JavaScript 事件驱动的交互模型不同于许多程序员习惯的请求-响应模型,但如你所见,它并不复杂。使用简单的消息队列和事件循环,JavaScript使得开发人员在构建他们的系统时使用大量asynchronously-fired(异步-触发)回调函数,让运行时环境能在等待外部事件触发的同时处理并发操作。然而,这不过是并发的一种方法。
以上就是本文的全部内容,希望对大家的学习有所帮助。