
We all heard about ajax and node.js. They are firmly established not only in the vocabulary, but also in the toolkit of the web developer. Ajax - asynchronous data pulling from server to page, node - asynchronous IO framework. But how is that asynchrony implemented in such a
single-threaded language like Javascript?
You, probably, have already guessed from the title, we will discuss the main loop (“main loop”).
As per specification
Let's start from afar. We say “Javascript”, we mean “ECMAScript”, we say “ES”, we mean “JS” (unless, of course, you are working in Mozilla and you are not called Brendan Ayh). It would be logical to start with the ECMAScript specification.
')
We open ECMA-262 and see ... that it says nothing at all about what the script should do after it has worked, how to pause or stop it. This means that it is given to the environment in which it is performed.
How it is done in WSH
Perhaps the most simple environment is Windows Script Host, also known as WSH. Loaded the processor 100%, worked to the end, died - this is such a simple life cycle. Of the execution control functions, only good old
sleep()
, which stops the interpreter for n milliseconds.
WScript.echo(new Date() + ': Hello world!'); WScript.sleep(1000); WScript.echo(new Date() + ': Goodbye, cruel world!');
For simple tasks, this is quite enough, the more difficult tasks can turn into a run with obstacles.
Example from the ceiling: the library has a terminal on Windows 98, without a keyboard, but with Internet Explorer 6, which shows a catalog of books in fullscreen. Young hooligans became obsessed with IE6, and it was not easy for librarians to start it back. How to be?
Create a function to start IE, create a callback with a sly name in the global osprey and launch it.
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();
Does not work. More precisely, it works, IE starts, but the interpreter modifies to the end and silently dies. We need to send him to eternal sleep. In the forehead,
sleep(Infinity)
WSH does not know how, so we make an infinite loop with slips for 60 seconds. It turns out something like this:
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); }
Here, now it works, the librarians are happy. Even better than expected - IE restarts immediately, and not after an arbitrary amount of time from 0 to 59 seconds. It turns out that the “event” OnQuit interrupts the interpreter’s sleep. Moreover, if
sleep
cut, leaving only the busy loop, the explorer will not even be able to run OnQuit.
Well, we got a primitive main loop. Briefly it can be described as:
- The interpreter does not complete with the completion of the program.
- The kollbek code can work only when the interpreter does nothing. At the same time two pieces of code can not work.
How it is done in browsers
The “fulfilled and died” approach in the browser is not suitable. Otherwise, how to use JS for its main purpose - to open annoying pop-up windows when you click anywhere? Browsers
(and node.js) have a main loop embedded. And so deeply that with the looping script, sometimes it is even impossible to use the interface.
The principle is simple: there is a queue, and any code that wants to execute becomes into it. If the queue is not empty, the interpreter bites out the first element from it and executes it. If the queue is empty, it waits until something hits it.
Any such “pieces of code” in the queue can be anything: embedded and linked scripts on the page, interface events (
onclick
,
onmouseover
, ...), timer callbacks (
setTimeout
,
setInterval
) or browser objects (
xhr.onreadystatechange
). In a sense, there is no difference between timers and events in browser JS.
Well, now in order.
alert
Three functions:
alert
,
prompt
and
confirm
- stand apart in all browser-based javascript. Perhaps you, like me, have become acquainted with JS with one of them. Anyway, each of them creates a modal window, and the interpreter falls asleep until it is closed. This is the only way to pause the main loop in the browser (without using a debugger).
Previously, in the
alert
browsers in some background tab could block the entire interface, and Google Chrome still suffers from this. However, in the modern Internet to meet the use of these functions can not often. Here you also do not use them.
setTimeout, setInterval
In the browser JS there is no
sleep
function - the interpreter never stops (except for
alert
and others like it). But you can postpone the execution of the function "on the spot" with the
setTimeout
function.
The syntax is simple and concise:
setTimeout(fn, timeout)
. The
fn
function will run no earlier than in
timeout
milliseconds. Why
not earlier , and not
exactly through ? Look under the hood.
A call to
setTimeout
registers a new timer (by the way, its identifier and this function returns when called). When his time expires, and it turns out that the interpreter is not busy at any time executing any code, the
fn
function will be called immediately, this is a trivial case.
If you are unlucky, and the JS engine will still chew on the pieces of the queue, then you will first have to wait for the queue to be empty. With all the desire to start a callback "right now" will not work, Javascript is single-threaded! Then, when the queue is empty, the interpreter runs over all the timers and checks which ones have expired. Of all the expired timers, the one that was set to a shorter timeout is selected and, if there are several, the one that was set before all is selected. Now, such a “most expired” timer generates a new element of the queue for execution, and - voila - the interpreter again has something to do - disassemble the queue that has become non-empty.
After the
setTimeout
timer
setTimeout
, it is deleted. That is,
setTimeout
does not fire twice.
Well, and some illustrative code:
console.log('script started'); setTimeout(function(){ console.log('timed out function'); }, 5); var endDate = +new Date() + 10; while (+new Date() < endDate){
The console will display:
script started
script finished
timed out function
In that order. Despite the fact that the timer ended after 5 milliseconds, the engine at that time was processing the super-important task of constantly comparing the date with the reference one. Therefore, the deferred function had to wait another 5 milliseconds until it was finished. Here it is. This is perhaps the most important.
You can cancel the timer at any time with the
clearTimeout(timeoutId)
function. If the timer has already bummed, then, in general, it is already meaningless to cancel it, but this is not considered a mistake.
And if we have time to cancel the timer?
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){
Such cases as in this example happen in practice rarely, but still. Both timers were set to 5 ms and both had expired when
while
finished its meaningless and merciless activity. But the first timer “shoots” first because it was set earlier, although the number of milliseconds of delay was the same for both timers. And successfully removes the already overdue second timer, not giving him a shot.
setInterval
differs from
setTimeout
in that its timer is not deleted after it generates an element for the execution queue. Instead, its value is reset to its original value. Thus, you can call a function periodically, without calling
setTimeout
inside
setTimeout
.
Note that the
setInterval
timer counter is not reset when the timer is triggered, but only when the main loop queue is empty. Because with this, he can "slip" over time.
Proof setInterval(function(){ console.log(+new Date()); }, 1000); setTimeout(function(){ var endDate = +new Date() + 2000; while (+new Date() < endDate){
The rest is all the same. Only intervals are canceled by another function,
clearInterval
.
Well, and finally, if
setTimeout
or
setInterval
was passed a timeout value less than 4 milliseconds, instead of it uses 4 ms exactly. For me it was an unpleasant surprise. But, apparently, it is for the better - an accidentally set interval of 0 milliseconds would quickly and effectively drown out the main loop of any browser. To execute a function with a truly zero timeout, use
setImmediate
. This feature is not yet widely available out of the box, but there are polyfills for it.
<script>
All scripts are in the queue of the main loop. On the one hand, it allows using
async
and
defer
. And on the other hand, and without these attributes, this can lead to unexpected results.
What if there are two scripts embedded in the page, and the first one sets the timeout to 4 milliseconds, and then slows down 10 milliseconds. When will the callback timeout work out - before the second script, or after?
Code <!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){ </script> <script> console.log('script 2'); </script>
Firefox, Chrome and IE10 will time out after the second script. Opera - before the second script.
And if the second script is not implemented, inline, and external?
Code <!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){ </script> <script src="http://127.0.0.1/script.js"></script>
And here all browsers
are likely to
time out before the second script. What is the use, you ask, of this knowledge, besides academic significance? To reduce the number of requests, often several scripts are merged into one. And the resulting script may work differently than the original.
Developments
It is worth considering separately UI events and DOM change events (
onDOMNodeInsertedIntoDocument
and the like).
Events occurring when the DOM tree changes do not fall into the main loop. Instead, they work out
immediately after changing the tree.
var i = 0; document.body.addEventListener('DOMSubtreeModified', function(e){ i = 42; }, false); console.log('i = ' + i);
Striving for such a spontaneous, it would seem, changing the value of a variable in real life is difficult. But I believe that some particularly tricky jQuery plugin, which actively uses MutationEvent and flows into the global skop, is waiting somewhere in the Internet. In addition, DOM change events are not recommended for use (deprecated) and very slow down work with the tree, so do not use them.
Let's return to mouse-keyboard-tachev events. Each such event has actions by default. For example, the default action for a button on a
mousedown
event is to get a depressed look. And for links to
click
- go to the address. Some default actions can be undone by calling the preventDefault method
preventDefault
the first argument in the callback function.
And that means two things. First, first the handler, then the default action, and the button will not “click” until the Javascript handler passes. Second, the handler will not start until its turn in the main loop comes up. Thus, massive calculations of the js engine have a very negative effect on what is called responsiveness. In other words, everything starts to significantly slow down and annoy the user. It would seem logical not to wait in the queue for an event for which no handler is hung, but in virtually all browsers the button with
onclick
slows down as well as without it.
Browser developers, of course, are trying to do something about it. Opera, for example, changes the visual representation, without waiting for the handler, which allows a little smooth brakes. In addition, the page may be affected by a heavy process in the adjacent tab, but this does not apply to Chrome.
The sequence “first handler, then default action” still follows the canonical case, when the keypress
event requires determining the length of the text in the input field, but it turns out that it does not coincide with the real one. I will leave this task to the reader.Webworks
WebWorkers are a way to unload the main loop, primarily with the goal of getting rid of the "sagging" in the responsiveness of the user interface. In fact, this is a separate process, with its own main loop, not tied to the interface and the DOM (and not having direct access to them).
WebWorker communicates with its “mainstream” process exclusively through messages. Moreover, when the message from the worker comes to the main process, it becomes - yes - yes - in the same queue as everything else.
In general, if you need to generate wav from xm on a client or unpack a multi-megabyte bzip, and the browser supports WebWorkers, use them.
How it is done in node.js
This section will be very small, because the node uses a very similar to the browser model. The same
setTimeout
and
setInterval
, and the events of the file system and other IO are similar to browser events, except that the arguments are different.
Of the features, the
process.nextTick
function can be noted, it works like
setTimeout(…, 0)
, but does not create any timers as superfluous. The identifier in the node.js timer is not an integer, as in browsers, but an object. Well, the node completely ignores the limit of 4 milliseconds.
Instead of conclusion
Summarizing all the above: understand how the main cycle works and try not to delay it for a long time.
For further reading / viewing: