What’s this post about?
With JavaScript approaching near-ubiquity as the scripting language of the web browser, it benefits you to have a basic understanding of its event-driven interaction model and how it differs from the request-response model typically found in languages like Ruby, Python, and Java. In this post, I’ll explain some core concepts of the JavaScript concurrency model, including its event loop and message queue in hopes of improving your understanding of a language you’re probably already writing but perhaps don’t fully understand.
随着网页浏览器脚本语言的javascript的日益普遍(几乎无所不在),了解它的时间驱动模式(event-driven interaction model )和请求响应模式(request-response model)的区别也变得更加有益.这篇文章中,我将解释一下javascript的核心观念(core concepts),包括,事件轮询( event loop),消息队列(message queue)希望能使你更加明白这门语言,可能你在用它,但你不太明白.
Who is this post for? (这篇文章为谁准备)
This post is aimed at web developers who are working with (or planning to work with) JavaScript in either the client or the server. If you’re already well-versed in event loops then much of this article will be familiar to you. For those of you who aren’t, I hope to provide you with a basic understanding such that you can better reason about the code you’re reading and writing day-to-day.
这篇文章的服务人群是js开发者,或是想做js开发的,不管前端还是后端.如果你非常精通js,你可能看到这篇文章的内容比较熟悉,对于不太明白的同学们,我希望这篇文章能让你在读代码和写代码上更上一层楼.
Non-blocking I/O(非阻塞I/O)
In JavaScript, almost all I/O is non-blocking. This includes HTTP requests, database operations and disk reads and writes; the single thread of execution asks the runtime to perform an operation, providing a callback function and then moves on to do something else. When the operation has been completed, a message is enqueued along with the provided callback function. At some point in the future, the message is dequeued and the callback fired.
在js中,几乎所有的I/O都是非阻塞(译者注:简单的说就是在读文件或数据库时,程序不暂停等待结果,而是继续执行,等读到了结果后去执行)的.包括HTTP请求,数据库操作还是文件读写.线程执行了一个I/O操作时返回一个回调函数,然后继续执行其他程序.当那个操作完成时,一个携带着回调函数的(完成)消息被加到消息队列,在后来某个点,这个消息出队,相应的回调函数被执行.
While this interaction model may be familiar for developers already accustomed to working with user interfaces – where events like “mousedown,” and “click” could be triggered at any time – it’s dissimilar to the synchronous, request-response model typically found in server-side applications.
对于已经习惯了UI(用户界面)的程序员来说,这种交互模式再熟悉不过了,比如,你可以随时单击,双击. 这种模式和典型的同步的请求响应模式是不一样的.Let’s compare two bits of code that make HTTP requests to www.google.com and output the response to console. First, Ruby, with Faraday:
让我们来用一个小程序来请求一下www.google.com,并打印一下结果,比较一下. 首先,看下Ruby,用Faraday(译者注:应该是HTTP类库):
1 2 3 |
response = Faraday.get 'http://www.google.com'
puts response
puts 'Done!'
|
The execution path is easy to follow:
执行顺序如下:
- The get method is executed and the thread of execution waits until a response is received
- The response is received from Google and returned to the caller where it’s stored in a variable
- The value of the variable (in this case, our response) is output to the console
- The value “Done!” is output to the console
1,get方法被执行并且这个执行线程阻塞(wait)着直到服务端响应.
2,从google响应的信息保存到一个变量(response)中
3,这个变量(response)的内容输出到控制台
4,"Done!" 输出到了控制台
Let’s do the same in JavaScript with Node.js and the Request library:
来让我们看看node.js的例子:
1 2 3 4 5 |
request('http://www.google.com', function(error, response, body) {
console.log(body);
});
console.log('Done!');
|
A slightly different look, and very different behavior:
看起来稍有不同,然而本质却大不相同:
- The request function is executed, passing an anonymous function as a callback to execute when a response is available sometime in the future.
- “Done!” is immediately output to the console
- Sometime in the future, the response comes back and our callback is executed, outputting its body to the console
1.请求执行时,将一个回调函数作为参数传过去,这个回调函数将被执行当响应返回时.
2."Done!" 被输出到控制台
3.响应返回时,那个回调函数被执行,body被输出到控制台
The Event Loop(事件轮循)
The decoupling of the caller from the response allows for the JavaScript runtime to do other things while waiting for your asynchronous operation to complete and their callbacks to fire. But where in memory do these callbacks live – and in what order are they executed? What causes them to be called?
响应回调函数允许javascript异步去做其它的事情,当等待响应的时候.回调函数总是在异步操作完成时被触发.
JavaScript runtimes contain a message queue which stores a list of messages to be processed and their associated callback functions. These messages are queued in response to external events (such as a mouse being clicked or receiving the response to an HTTP request) given a callback function has been provided. If, for example a user were to click a button and no callback function was provided – no message would have been enqueued.
js运行时有一个消息队列和一个与之关联的回调函数.事件发生时(像点击鼠标),一个被提供回调函数的消息入队.如果一个用户点击了按钮,而这个按钮没有回调函数,那么就没有消息入队.
In a loop, the queue is polled for the next message (each poll referred to as a “tick”) and when a message is encountered, the callback for that message is executed.
事件轮循中,队列一直循环(每一次循环称为一个"tick"),当事件被触发时,回调函数被执行.
The calling of this callback function serves as the initial frame in the call stack, and due to JavaScript being single-threaded, further message polling and processing is halted pending the return of all calls on the stack. Subsequent (synchronous) function calls add new call frames to the stack (for example, function init calls function changeColor).
在调用栈中,这个回调函数的调用作为初始,并且由于javascript单线程,下一个的消息和主进程都会停止.
1 2 3 4 5 6 7 8 9 |
function init() {
var link = document.getElementById("foo");
link.addEventListener("click", function changeColor() {
this.style.color = "burlywood";
});
}
init();
|
In this example, a message (and callback, changeColor) is enqueued when the user clicks on the ‘foo’ element and an the “onclick” event fires. When the message is dequeued, its callback function changeColor is called. When changeColor returns (or an error is thrown), the event loop continues. As long as function changeColor exists, specified as the onclick callback for the ‘foo’ element, subsequent clicks on the element will cause more messages (and associated callback changeColor) to become enqueued.
这个例子中,当用户点击了‘foo'元素,消息(即改变颜色的那个回调函数)被入队。当消息出队的时候,改变颜色(changeColor)函数被执行。changeColor函数执行完返回(或抛出错误)了,事件论询继续执行。
Queuing Additional Messages
If a function called in your code is asynchronous (like setTimeout), the provided callback will ultimately be executed as part of a different queued message, on some future tick of the event loop. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function f() {
console.log("foo");
setTimeout(g, 0);
console.log("baz");
h();
}
function g() {
console.log("bar");
}
function h() {
console.log("blix");
}
f();
|
Due to the non-blocking nature of setTimeout, its callback will fire at least 0 milliseconds in the future and is not processed as part of this message. In this example, setTimeout is invoked, passing a callback function g and a timeout of 0 milliseconds. When the specified time elapses (in this case, almost instantly) a separate message will be enqueued containing g as its callback function. The resulting console activity would look like: “foo”, “baz”, “blix” and then on the next tick of the event loop: “bar”. If in the same call frame two calls are made to setTimeout – passing the same value for a second argument – their callbacks will be queued in the order of invocation.
Web Workers
Using Web Workers enables you to offload an expensive operation to a separate thread of execution, freeing up the main thread to do other things. The worker includes a separate message queue, event loop, and memory space independent from the original thread that instantiated it. Communication between the worker and the main thread is done via message passing, which looks very much like the traditional, evented code-examples we’ve already seen.
First, our worker:
1 2 3 4 5 6 7 |
// our worker, which does some CPU-intensive operation
var reportResult = function(e) {
pi = SomeLib.computePiToSpecifiedDecimals(e.data);
postMessage(pi);
};
onmessage = reportResult;
|
Then, the main chunk of code that lives in a script-tag in our HTML:
1 2 3 4 5 6 7 8 |
// 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);
|
In this example, the main thread spawns a worker and registers the logResult callback function to the its “message” event. In the worker, the reportResult function is registered to its own “message” event. When the worker thread receives the message from the main thread, the worker enqueues a message and corresponding reportResult callback. When dequeued, a message is posted back to the main thread where a new message is enqueued (along with the logResult callback). In this way the developer can delegate CPU-intensive operations to a separate thread, freeing the main thread up to continue processing messages and handling events.
A Note on Closures
JavaScript’s support for closures allow you to register callbacks that, when executed, maintain access to the environment in which they were created even though the execution of the callback creates a new call stack entirely. This is particularly of interest knowing that our callbacks are called as part of a different message than the one in which they were created. Consider the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function changeHeaderDeferred() {
var header = document.getElementById("header");
setTimeout(function changeHeader() {
header.style.color = "red";
return false;
}, 100);
return false;
}
changeHeaderDeferred();
|
In this example, the changeHeaderDeferred function is executed which includes variable header. The function setTimeout is invoked, which causes a message (plus the changeHeader callback) to be added to the message queue approximately 100 milliseconds in the future. The changeHeaderDeferred function then returns false, ending the processing of the first message – but the header variable is still referenced via a closure and is not garbage collected. When the second message is processed (the changeHeader function) it maintains access to the header variable declared in the outer function’s scope. Once the second message (the changeHeader function) is processed, the header variable can be garbage collected.
Takeaways
JavaScript’s event-driven interaction model differs from the request-response model many programmers are accustomed to – but as you can see, it’s not rocket science. With a simple message queue and event loop, JavaScript enables a developer to build their system around a collection of asynchronously-fired callbacks, freeing the runtime to handle concurrent operations while waiting on external events to happen. However, this is but one approach to concurrency. In the second part of this article I’ll compare JavaScript’s concurrency model with those found in MRI Ruby (with threads and the GIL), EventMachine (Ruby), and Java (threads).
Additional Reading
- Check out this presentation I did recently, titled “The JavaScript Event Loop: Concurrency in the Language of the Web”
- Concurrency model and Event Loop @ MDN
- An intro to the Node.js platform, by Aaron Stannard
http://blog.carbonfive.com/2013/10/27/the-javascript-event-loop-explained/